diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index a09189e3..fbe45006 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -8,8 +8,8 @@ body: id: gait_version attributes: label: Gait version - description: Output of `gait --version` - placeholder: gait version 1.0.0 + description: Output of `gait version --json` (authoritative install probe) + placeholder: '{"ok":true,"version":"1.3.6"}' validations: required: true - type: input diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 83bc793a..8ebe8e9f 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -16,8 +16,8 @@ body: id: gait_version attributes: label: Gait version - description: Output of `gait --version` if installed. - placeholder: gait version 1.0.0 + description: Output of `gait version --json` if installed. + placeholder: '{"ok":true,"version":"1.3.6"}' validations: required: false - type: textarea diff --git a/.gitignore b/.gitignore index 744afc0e..13338d59 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ perf/ui_budget_report.json /fixtures/ /.uat_local/ /.tmp/ +/.cache/ docs-site/node_modules/ docs-site/.next/ docs-site/out/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ee695a..5d9938ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Gate intent normalization now treats omitted target `discovery_method` as `unknown` instead of empty so policies can deterministically match unknown/dynamic discovery paths. - Durable job resume now preserves the originally bound identity and rejects attempts to resume with a different identity. +- `gait version --json` now prefers clean release metadata for promoted install paths while keeping repo-local contributor builds on the explicit `0.0.0-dev` fallback. +- Public onboarding/docs copy now treats LangChain as the official middleware lane and the OpenAI example as a reference boundary demo. ### Upgrade Notes diff --git a/Makefile b/Makefile index 72c02362..1d830060 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ BENCH_BASELINE ?= perf/bench_baseline.json .PHONY: fmt lint lint-fast codeql test test-fast test-scenarios prepush prepush-full github-guardrails github-guardrails-strict test-hardening test-hardening-acceptance test-chaos test-e2e test-acceptance test-v1-6-acceptance test-v1-7-acceptance test-v1-8-acceptance test-v2-3-acceptance test-v2-4-acceptance test-v2-5-acceptance test-v2-6-acceptance test-voice-acceptance test-context-conformance test-context-chaos test-packspec-tck test-script-intent-acceptance test-ui-acceptance test-ui-unit test-ui-e2e-smoke test-ui-perf test-claude-code-hook-contract test-adoption test-adapter-parity test-ecosystem-automation test-release-smoke test-install test-install-path-versions test-contracts test-intent-receipt-conformance test-ci-regress-template test-ci-portability-templates test-live-connectors test-skill-supply-chain test-runtime-slo test-ent-consumer-contract test-uat-local test-openclaw-skill-install test-beads-bridge test-docs-storyline test-docs-consistency test-demo-recording test-github-action-runtime-guard openclaw-skill-install build bench bench-check bench-budgets context-budgets skills-validate ecosystem-validate ecosystem-release-notes demo-90s demo-hero-gif homebrew-formula wiki-publish tool-allowlist-policy ui-build ui-sync ui-deps-check .PHONY: hooks -.PHONY: docs-site-install docs-site-build docs-site-lint docs-site-check +.PHONY: docs-site-install docs-site-build docs-site-lint docs-site-check docs-site-validate fmt: gofmt -w . @@ -306,17 +306,24 @@ openclaw-skill-install: @if [ -z "$(TARGET_DIR)" ]; then bash scripts/install_openclaw_skill.sh; else bash scripts/install_openclaw_skill.sh --target-dir "$(TARGET_DIR)"; fi docs-site-install: - cd docs-site && npm ci + mkdir -p .cache/npm + cd docs-site && NPM_CONFIG_CACHE=../.cache/npm npm ci -docs-site-build: +docs-site-build: docs-site-install cd docs-site && npm run build -docs-site-lint: +docs-site-lint: docs-site-install cd docs-site && npm run lint docs-site-check: $(PYTHON) scripts/check_docs_site_validation.py --report gait-out/docs_site_validation_report.json +docs-site-validate: + mkdir -p .cache/npm + cd docs-site && NPM_CONFIG_CACHE=../.cache/npm npm ci && npm run lint && npm run build + $(PYTHON) scripts/check_docs_site_validation.py --report gait-out/docs_site_validation_report.json + rm -rf docs-site/node_modules + ui-build: bash scripts/ui_build.sh diff --git a/README.md b/README.md index 40475dbc..24efbae7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,13 @@ Managed/preloaded agent note: if you cannot intercept tool execution before side ## Install -Choose one install path: +Choose one install path. Prefer release binaries for onboarding and support flows, then confirm the installed version with `gait version --json`. + +### Release Installer + +```bash +curl -fsSL https://raw.githubusercontent.com/Clyra-AI/gait/main/scripts/install.sh | bash +``` ### Homebrew @@ -30,11 +36,7 @@ brew install Clyra-AI/tap/gait go install github.com/Clyra-AI/gait/cmd/gait@latest ``` -### Release Installer - -```bash -curl -fsSL https://raw.githubusercontent.com/Clyra-AI/gait/main/scripts/install.sh | bash -``` +Tagged module installs resolve the release version from Go build metadata. Local checkout builds remain contributor/dev builds and may still report `0.0.0-dev`. ## Start Here @@ -73,10 +75,10 @@ This path writes `.gait.yaml`, reports the live policy contract, and returns ins Use this when your agent already makes real tool calls and you want enforcement at the execution seam. -Official lanes: +Official and reference lanes: -- OpenAI Agents wrapper lane: [`examples/integrations/openai_agents/`](examples/integrations/openai_agents/) -- LangChain middleware lane: [`examples/integrations/langchain/`](examples/integrations/langchain/) +- LangChain middleware lane (official): [`examples/integrations/langchain/`](examples/integrations/langchain/) +- OpenAI-style reference boundary demo: [`examples/integrations/openai_agents/`](examples/integrations/openai_agents/) Other supported boundary paths: @@ -131,9 +133,9 @@ This is the core contract across wrappers, middleware, sidecars, and MCP boundar ## Runtime Integration Paths -### OpenAI Agents +### OpenAI Agents Reference Demo -This is the blessed top-of-funnel runtime lane: a local wrapper at the tool boundary with deterministic allow, block, and approval quickstarts. +This is the fastest in-repo reference demo for the runtime boundary contract: a local wrapper at the tool boundary with deterministic allow, block, and approval quickstarts. It is not a package-backed official SDK lane. ```bash python3 examples/integrations/openai_agents/quickstart.py --scenario allow @@ -174,7 +176,7 @@ gait enforce --json -- ## Simple End-To-End Scenario -See [`docs/scenarios/simple_agent_tool_boundary.md`](docs/scenarios/simple_agent_tool_boundary.md) and the promoted wrapper quickstart at `examples/integrations/openai_agents/quickstart.py`. +See [`docs/scenarios/simple_agent_tool_boundary.md`](docs/scenarios/simple_agent_tool_boundary.md) and the reference wrapper quickstart at `examples/integrations/openai_agents/quickstart.py`. ## Policy Onboarding diff --git a/cmd/gait/approve.go b/cmd/gait/approve.go index d9ab030d..25345adb 100644 --- a/cmd/gait/approve.go +++ b/cmd/gait/approve.go @@ -95,7 +95,7 @@ func runApprove(arguments []string) int { } result, err := gate.MintApprovalToken(gate.MintApprovalTokenOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), ApproverIdentity: approver, ReasonCode: reasonCode, IntentDigest: intentDigest, diff --git a/cmd/gait/approve_script.go b/cmd/gait/approve_script.go index 7f89dafc..089c680e 100644 --- a/cmd/gait/approve_script.go +++ b/cmd/gait/approve_script.go @@ -122,7 +122,7 @@ func runApproveScript(arguments []string) int { SchemaID: "gait.gate.approved_script_entry", SchemaVersion: "1.0.0", CreatedAt: nowUTC, - ProducerVersion: version, + ProducerVersion: currentVersion(), PatternID: strings.TrimSpace(patternID), PolicyDigest: policyDigest, ScriptHash: scriptHash, diff --git a/cmd/gait/delegate.go b/cmd/gait/delegate.go index b5751ab4..1e8bc195 100644 --- a/cmd/gait/delegate.go +++ b/cmd/gait/delegate.go @@ -109,7 +109,7 @@ func runDelegateMint(arguments []string) int { } result, err := gate.MintDelegationToken(gate.MintDelegationTokenOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), DelegatorIdentity: delegator, DelegateIdentity: delegate, Scope: scopeValues, diff --git a/cmd/gait/demo.go b/cmd/gait/demo.go index e32af3b6..4d10efc0 100644 --- a/cmd/gait/demo.go +++ b/cmd/gait/demo.go @@ -240,7 +240,7 @@ func executeDurableDemo() (demoOutput, int) { baseNow := time.Date(2026, time.February, 14, 0, 0, 0, 0, time.UTC) if _, err := jobruntime.Submit(jobRoot, jobruntime.SubmitOptions{ JobID: demoDurableJobID, - ProducerVersion: version, + ProducerVersion: currentVersion(), EnvironmentFingerprint: "envfp:demo-durable", Actor: "demo.user", Now: baseNow, @@ -301,7 +301,7 @@ func executeDurableDemo() (demoOutput, int) { if err := os.Remove(packPath); err != nil && !errors.Is(err, os.ErrNotExist) { return demoOutput{OK: false, Mode: string(demoModeDurable), Error: err.Error()}, exitCodeForError(err, exitInvalidInput) } - if _, err := pack.BuildJobPackFromPath(jobRoot, demoDurableJobID, packPath, version, nil); err != nil { + if _, err := pack.BuildJobPackFromPath(jobRoot, demoDurableJobID, packPath, currentVersion(), nil); err != nil { return demoOutput{OK: false, Mode: string(demoModeDurable), Error: err.Error()}, exitCodeForError(err, exitInvalidInput) } verifyResult, err := pack.Verify(packPath, pack.VerifyOptions{}) @@ -365,7 +365,7 @@ func executePolicyDemo() (demoOutput, int) { SchemaID: "gait.gate.intent_request", SchemaVersion: "1.0.0", CreatedAt: time.Date(2026, time.February, 14, 0, 0, 0, 0, time.UTC), - ProducerVersion: version, + ProducerVersion: currentVersion(), ToolName: "tool.delete", Args: map[string]any{"path": "/tmp/demo/delete-me.txt"}, Targets: []schemagate.IntentTarget{{ @@ -390,7 +390,7 @@ func executePolicyDemo() (demoOutput, int) { return demoOutput{OK: false, Mode: string(demoModePolicy), Error: err.Error()}, exitCodeForError(err, exitInvalidInput) } - evalResult, err := gatecore.EvaluatePolicyDetailed(policy, intent, gatecore.EvalOptions{ProducerVersion: version}) + evalResult, err := gatecore.EvaluatePolicyDetailed(policy, intent, gatecore.EvalOptions{ProducerVersion: currentVersion()}) if err != nil { return demoOutput{OK: false, Mode: string(demoModePolicy), Error: err.Error()}, exitCodeForError(err, exitInvalidInput) } @@ -444,7 +444,7 @@ func buildDemoRunpack() (schemarunpack.Run, []schemarunpack.IntentRecord, []sche SchemaID: "gait.runpack.run", SchemaVersion: "1.0.0", CreatedAt: ts, - ProducerVersion: "0.0.0-dev", + ProducerVersion: currentVersion(), RunID: demoRunID, Env: schemarunpack.RunEnv{ OS: "demo", diff --git a/cmd/gait/doctor.go b/cmd/gait/doctor.go index 70d149a0..e7420e1c 100644 --- a/cmd/gait/doctor.go +++ b/cmd/gait/doctor.go @@ -82,7 +82,7 @@ func runDoctor(arguments []string) int { result := doctor.Run(doctor.Options{ WorkDir: workDir, OutputDir: outputDir, - ProducerVersion: version, + ProducerVersion: currentVersion(), KeyMode: sign.KeyMode(strings.ToLower(strings.TrimSpace(keyMode))), KeyConfig: sign.KeyConfig{ PrivateKeyPath: privateKeyPath, @@ -151,7 +151,7 @@ func runDoctorAdoption(arguments []string) int { if err != nil { return writeDoctorAdoptionOutput(jsonOutput, doctorAdoptionOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) } - report := scout.BuildAdoptionReport(events, fromPath, version, time.Time{}) + report := scout.BuildAdoptionReport(events, fromPath, currentVersion(), time.Time{}) return writeDoctorAdoptionOutput(jsonOutput, doctorAdoptionOutput{ OK: true, Report: &report, diff --git a/cmd/gait/gate.go b/cmd/gait/gate.go index 809cc727..d13d1901 100644 --- a/cmd/gait/gate.go +++ b/cmd/gait/gate.go @@ -394,7 +394,7 @@ func runGateEval(arguments []string) int { } startupWarnings = append(startupWarnings, "approved script fast-path evaluation failed; continuing with policy evaluation") } else if match.Matched { - preApprovedOutcome, preApproveErr := buildPreApprovedOutcome(intent, version, match) + preApprovedOutcome, preApproveErr := buildPreApprovedOutcome(intent, currentVersion(), match) if preApproveErr != nil { return writeGateEvalOutput(jsonOutput, gateEvalOutput{OK: false, Error: preApproveErr.Error()}, exitCodeForError(preApproveErr, exitInvalidInput)) } @@ -407,7 +407,7 @@ func runGateEval(arguments []string) int { } if !preApprovedFastPath { outcome, err = gate.EvaluatePolicyDetailed(policy, intent, gate.EvalOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), WrkrInventory: wrkrInventory, WrkrSource: wrkrSource, VerifiedContextEnvelope: verifiedContextEnvelope, @@ -716,7 +716,7 @@ func runGateEval(arguments []string) int { exitCode = gateEvalExitCodeForVerdict(result.Verdict, exitCode) traceResult, err := gate.EmitSignedTrace(policy, preparedIntent, result, gate.EmitTraceOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), CorrelationID: currentCorrelationID(), ApprovalTokenRef: resolvedApprovalRef, DelegationTokenRef: resolvedDelegationRef, @@ -745,7 +745,7 @@ func runGateEval(arguments []string) int { } audit := gate.BuildApprovalAuditRecord(gate.BuildApprovalAuditOptions{ CreatedAt: result.CreatedAt, - ProducerVersion: version, + ProducerVersion: currentVersion(), TraceID: traceResult.Trace.TraceID, ToolName: traceResult.Trace.ToolName, IntentDigest: traceResult.IntentDigest, @@ -765,7 +765,7 @@ func runGateEval(arguments []string) int { } credentialRecord := gate.BuildBrokerCredentialRecord(gate.BuildBrokerCredentialRecordOptions{ CreatedAt: result.CreatedAt, - ProducerVersion: version, + ProducerVersion: currentVersion(), TraceID: traceResult.Trace.TraceID, ToolName: traceResult.Trace.ToolName, Identity: preparedIntent.Context.Identity, @@ -788,7 +788,7 @@ func runGateEval(arguments []string) int { } audit := gate.BuildDelegationAuditRecord(gate.BuildDelegationAuditOptions{ CreatedAt: result.CreatedAt, - ProducerVersion: version, + ProducerVersion: currentVersion(), TraceID: traceResult.Trace.TraceID, ToolName: traceResult.Trace.ToolName, IntentDigest: traceResult.IntentDigest, diff --git a/cmd/gait/gateway.go b/cmd/gait/gateway.go index f1e0e88e..530218fd 100644 --- a/cmd/gait/gateway.go +++ b/cmd/gait/gateway.go @@ -103,7 +103,7 @@ func runGatewayIngest(arguments []string) int { Source: strings.TrimSpace(source), LogPath: strings.TrimSpace(logPath), OutputPath: strings.TrimSpace(proofOut), - ProducerVersion: version, + ProducerVersion: currentVersion(), SigningPrivateKey: keyPair.Private, }) if err != nil { diff --git a/cmd/gait/guard.go b/cmd/gait/guard.go index a1d3a1ba..69869366 100644 --- a/cmd/gait/guard.go +++ b/cmd/gait/guard.go @@ -187,7 +187,7 @@ func runGuardPack(arguments []string) int { TemplateID: templateID, RenderPDF: renderPDF, AutoDiscoverV12: true, - ProducerVersion: version, + ProducerVersion: currentVersion(), SignKey: keyPair.Private, }) if err != nil { @@ -373,7 +373,7 @@ func runGuardRetain(arguments []string) int { PackTTL: parsedPackTTL, DryRun: dryRun, ReportOutput: reportPath, - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if err != nil { return writeGuardRetainOutput(jsonOutput, guardRetainOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) @@ -440,7 +440,7 @@ func runGuardEncrypt(arguments []string) int { KeyEnv: keyEnv, KeyCommand: keyCommand, KeyCommandArgs: parseCSVList(keyCommandArgs), - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if err != nil { return writeGuardEncryptOutput(jsonOutput, guardEncryptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) diff --git a/cmd/gait/incident.go b/cmd/gait/incident.go index 6e47f426..b245424d 100644 --- a/cmd/gait/incident.go +++ b/cmd/gait/incident.go @@ -107,7 +107,7 @@ func runIncidentPack(arguments []string) int { Window: parsedWindow, TemplateID: templateID, RenderPDF: renderPDF, - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if err != nil { return writeIncidentPackOutput(jsonOutput, incidentPackOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) diff --git a/cmd/gait/job.go b/cmd/gait/job.go index 9efd5f94..1eb0d95b 100644 --- a/cmd/gait/job.go +++ b/cmd/gait/job.go @@ -118,7 +118,7 @@ func runJobSubmit(arguments []string) int { JobID: strings.TrimSpace(jobID), Actor: strings.TrimSpace(actor), Identity: strings.TrimSpace(identity), - ProducerVersion: version, + ProducerVersion: currentVersion(), EnvironmentFingerprint: jobruntime.EnvironmentFingerprint(envFingerprint), PolicyDigest: resolvedPolicyDigest, PolicyRef: resolvedPolicyRef, diff --git a/cmd/gait/main.go b/cmd/gait/main.go index f7007897..bce43c75 100644 --- a/cmd/gait/main.go +++ b/cmd/gait/main.go @@ -60,7 +60,7 @@ func run(arguments []string) int { func runDispatch(arguments []string) int { if len(arguments) < 2 { - fmt.Println("gait", version) + fmt.Println("gait", currentVersion()) return exitOK } if isTopLevelHelp(arguments[1]) { @@ -159,10 +159,10 @@ func runVersion(arguments []string) int { if hasJSONFlag(arguments) { return writeJSONOutput(versionOutput{ OK: true, - Version: version, + Version: currentVersion(), }, exitOK) } - fmt.Println("gait", version) + fmt.Println("gait", currentVersion()) return exitOK } @@ -198,7 +198,7 @@ func writeAdoptionEvent(command string, exitCode int, elapsed time.Duration, now return } workflowID := strings.TrimSpace(os.Getenv("GAIT_ADOPTION_WORKFLOW")) - event := scout.NewAdoptionEvent(command, exitCode, elapsed, version, now, workflowID) + event := scout.NewAdoptionEvent(command, exitCode, elapsed, currentVersion(), now, workflowID) recordTelemetryWriteOutcome("adoption", scout.AppendAdoptionEvent(adoptionPath, event)) } @@ -207,7 +207,7 @@ func writeOperationalEventStart(command string, correlationID string, now time.T if operationalPath == "" { return } - event := scout.NewOperationalStartEvent(command, correlationID, version, now) + event := scout.NewOperationalStartEvent(command, correlationID, currentVersion(), now) recordTelemetryWriteOutcome("operational_start", scout.AppendOperationalEvent(operationalPath, event)) } @@ -223,7 +223,7 @@ func writeOperationalEventEnd(command string, correlationID string, exitCode int category = string(resolvedCategory) retryable = defaultRetryable(resolvedCategory) } - event := scout.NewOperationalEndEvent(command, correlationID, version, exitCode, category, retryable, elapsed, now) + event := scout.NewOperationalEndEvent(command, correlationID, currentVersion(), exitCode, category, retryable, elapsed, now) recordTelemetryWriteOutcome("operational_end", scout.AppendOperationalEvent(operationalPath, event)) } @@ -255,7 +255,7 @@ func recordTelemetryWriteOutcome(stream string, err error) { SchemaID: "gait.scout.telemetry_health", SchemaVersion: "1.0.0", CreatedAt: time.Now().UTC().Format(time.RFC3339Nano), - ProducerVersion: version, + ProducerVersion: currentVersion(), Streams: make(map[string]telemetryStreamHealth, len(telemetryState.streams)), } for key, value := range telemetryState.streams { diff --git a/cmd/gait/main_test.go b/cmd/gait/main_test.go index fe84fd14..b0aa0054 100644 --- a/cmd/gait/main_test.go +++ b/cmd/gait/main_test.go @@ -265,7 +265,7 @@ func TestRunDispatchVersionJSONAliases(t *testing.T) { } expected := versionOutput{ OK: true, - Version: version, + Version: currentVersion(), } if output != expected { t.Fatalf("unexpected version output: got=%#v want=%#v", output, expected) @@ -291,7 +291,7 @@ func TestRunDispatchVersionTextAliases(t *testing.T) { t.Fatalf("run version text: expected %d got %d", exitOK, code) } }) - if strings.TrimSpace(raw) != "gait "+version { + if strings.TrimSpace(raw) != "gait "+currentVersion() { t.Fatalf("unexpected version text: %q", raw) } }) diff --git a/cmd/gait/mcp.go b/cmd/gait/mcp.go index ee9ba619..712729ce 100644 --- a/cmd/gait/mcp.go +++ b/cmd/gait/mcp.go @@ -375,7 +375,7 @@ func evaluateMCPProxyPayload(policyPath string, payload []byte, options mcpProxy if err != nil { return mcpProxyOutput{}, exitInvalidInput, err } - evalOptions := gate.EvalOptions{ProducerVersion: version} + evalOptions := gate.EvalOptions{ProducerVersion: currentVersion()} envelopePath := strings.TrimSpace(options.ContextEnvelopePath) if options.VerifiedContextEnvelope != nil { evalOptions.VerifiedContextEnvelope = options.VerifiedContextEnvelope @@ -445,7 +445,7 @@ func evaluateMCPProxyPayload(policyPath string, payload []byte, options mcpProxy resolvedTracePath = fmt.Sprintf("trace_%s_%s.json", normalizeRunID(options.RunID), time.Now().UTC().Format("20060102T150405.000000000")) } traceResult, err := gate.EmitSignedTrace(policy, evalResult.Intent, evalResult.Outcome.Result, gate.EmitTraceOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), ContextSource: evalResult.Outcome.ContextSource, CompositeRiskClass: evalResult.Outcome.CompositeRiskClass, StepVerdicts: evalResult.Outcome.StepVerdicts, @@ -504,7 +504,7 @@ func evaluateMCPProxyPayload(policyPath string, payload []byte, options mcpProxy buildResult, buildErr := pack.BuildRunPack(pack.BuildRunOptions{ RunpackPath: runpackPathForPack, OutputPath: resolvedPackPath, - ProducerVersion: version, + ProducerVersion: currentVersion(), SigningPrivateKey: keyPair.Private, }) cleanup() @@ -517,7 +517,7 @@ func evaluateMCPProxyPayload(policyPath string, payload []byte, options mcpProxy exportEvent := mcp.ExportEvent{ CreatedAt: evalResult.Outcome.Result.CreatedAt, - ProducerVersion: version, + ProducerVersion: currentVersion(), RunID: resolvedRunID, SessionID: evalResult.Intent.Context.SessionID, TraceID: traceResult.Trace.TraceID, @@ -772,7 +772,7 @@ func writeMCPRunpack(path string, runID string, evalResult mcp.EvalResult, trace SchemaID: "gait.runpack.run", SchemaVersion: "1.0.0", CreatedAt: now, - ProducerVersion: version, + ProducerVersion: currentVersion(), RunID: runID, Timeline: []schemarunpack.TimelineEvt{ {Event: "proxy_eval_start", TS: now}, @@ -783,7 +783,7 @@ func writeMCPRunpack(path string, runID string, evalResult mcp.EvalResult, trace SchemaID: "gait.runpack.intent", SchemaVersion: "1.0.0", CreatedAt: now, - ProducerVersion: version, + ProducerVersion: currentVersion(), RunID: runID, IntentID: "intent_1", ToolName: evalResult.Intent.ToolName, @@ -794,7 +794,7 @@ func writeMCPRunpack(path string, runID string, evalResult mcp.EvalResult, trace SchemaID: "gait.runpack.result", SchemaVersion: "1.0.0", CreatedAt: now, - ProducerVersion: version, + ProducerVersion: currentVersion(), RunID: runID, IntentID: "intent_1", Status: resultStatus, @@ -805,7 +805,7 @@ func writeMCPRunpack(path string, runID string, evalResult mcp.EvalResult, trace SchemaID: "gait.runpack.refs", SchemaVersion: "1.0.0", CreatedAt: now, - ProducerVersion: version, + ProducerVersion: currentVersion(), RunID: runID, Receipts: []schemarunpack.RefReceipt{}, }, diff --git a/cmd/gait/mcp_server.go b/cmd/gait/mcp_server.go index e0f3b058..73d73b38 100644 --- a/cmd/gait/mcp_server.go +++ b/cmd/gait/mcp_server.go @@ -496,7 +496,7 @@ func evaluateMCPServeRequest(config mcpServeConfig, writer http.ResponseWriter, if _, err := runpack.StartSession(journalPath, runpack.SessionStartOptions{ SessionID: sessionID, RunID: output.RunID, - ProducerVersion: version, + ProducerVersion: currentVersion(), }); err != nil { if strings.TrimSpace(input.SessionJournal) == "" && strings.Contains(err.Error(), "different session/run") { base := sanitizeSessionFileBase(sessionID) @@ -504,7 +504,7 @@ func evaluateMCPServeRequest(config mcpServeConfig, writer http.ResponseWriter, if _, retryErr := runpack.StartSession(journalPath, runpack.SessionStartOptions{ SessionID: sessionID, RunID: output.RunID, - ProducerVersion: version, + ProducerVersion: currentVersion(), }); retryErr != nil { return mcpServeEvaluateResponse{}, retryErr } @@ -525,7 +525,7 @@ func evaluateMCPServeRequest(config mcpServeConfig, writer http.ResponseWriter, agentChain = append(agentChain, output.Relationship.AgentChain...) } event, err := runpack.AppendSessionEvent(journalPath, runpack.SessionAppendOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), ToolName: output.ToolName, IntentDigest: output.IntentDigest, PolicyDigest: output.PolicyDigest, @@ -553,7 +553,7 @@ func evaluateMCPServeRequest(config mcpServeConfig, writer http.ResponseWriter, if input.CheckpointInterval > 0 && event.Sequence%int64(input.CheckpointInterval) == 0 && config.RunpackDir != "" { checkpointOut := filepath.Join(config.RunpackDir, fmt.Sprintf("%s_cp_%06d.zip", sanitizeSessionFileBase(sessionID), event.Sequence)) _, chainPath, checkpointErr := runpack.SessionCheckpointAndWriteChain(journalPath, checkpointOut, runpack.SessionCheckpointOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if checkpointErr != nil { return mcpServeEvaluateResponse{}, checkpointErr diff --git a/cmd/gait/onboarding.go b/cmd/gait/onboarding.go index 6fd72843..bf163e4d 100644 --- a/cmd/gait/onboarding.go +++ b/cmd/gait/onboarding.go @@ -493,7 +493,7 @@ func currentSurfaceContract() surfaceContract { } func detectRepoSurface(root string) (repoDetection, error) { - provider := scout.DefaultProvider{Options: scout.SnapshotOptions{ProducerVersion: version}} + provider := scout.DefaultProvider{Options: scout.SnapshotOptions{ProducerVersion: currentVersion()}} snapshot, err := provider.Snapshot(context.Background(), scout.SnapshotRequest{Roots: []string{root}}) if err != nil { return repoDetection{}, fmt.Errorf("detect repo surface: %w", err) diff --git a/cmd/gait/pack.go b/cmd/gait/pack.go index abc81deb..3fe459c2 100644 --- a/cmd/gait/pack.go +++ b/cmd/gait/pack.go @@ -164,7 +164,7 @@ func runPackBuild(arguments []string) int { result, buildErr := pack.BuildRunPack(pack.BuildRunOptions{ RunpackPath: runPath, OutputPath: strings.TrimSpace(outPath), - ProducerVersion: version, + ProducerVersion: currentVersion(), SigningPrivateKey: keyPair.Private, }) if buildErr != nil { @@ -176,7 +176,7 @@ func runPackBuild(arguments []string) int { if resolveErr != nil { return writePackOutput(jsonOutput, packOutput{OK: false, Operation: "build", Error: resolveErr.Error()}, exitCodeForError(resolveErr, exitInvalidInput)) } - result, buildErr := pack.BuildJobPackFromPath(root, jobID, strings.TrimSpace(outPath), version, keyPair.Private) + result, buildErr := pack.BuildJobPackFromPath(root, jobID, strings.TrimSpace(outPath), currentVersion(), keyPair.Private) if buildErr != nil { return writePackOutput(jsonOutput, packOutput{OK: false, Operation: "build", Error: buildErr.Error()}, exitCodeForError(buildErr, exitInvalidInput)) } @@ -185,7 +185,7 @@ func runPackBuild(arguments []string) int { result, buildErr := pack.BuildCallPack(pack.BuildCallOptions{ CallRecordPath: strings.TrimSpace(from), OutputPath: strings.TrimSpace(outPath), - ProducerVersion: version, + ProducerVersion: currentVersion(), SigningPrivateKey: keyPair.Private, }) if buildErr != nil { diff --git a/cmd/gait/policy.go b/cmd/gait/policy.go index 0f3d750e..9ba7b7ab 100644 --- a/cmd/gait/policy.go +++ b/cmd/gait/policy.go @@ -461,7 +461,7 @@ func runPolicySimulate(arguments []string) int { baselineRun, baselineErr := policytest.Run(policytest.RunOptions{ Policy: baselinePolicy, Intent: intent, - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if baselineErr != nil { return writePolicySimulateOutput(jsonOutput, policySimulateOutput{OK: false, Error: baselineErr.Error()}, exitCodeForError(baselineErr, exitInvalidInput)) @@ -469,7 +469,7 @@ func runPolicySimulate(arguments []string) int { candidateRun, candidateErr := policytest.Run(policytest.RunOptions{ Policy: candidatePolicy, Intent: intent, - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if candidateErr != nil { return writePolicySimulateOutput(jsonOutput, policySimulateOutput{OK: false, Error: candidateErr.Error()}, exitCodeForError(candidateErr, exitInvalidInput)) @@ -561,7 +561,7 @@ func runPolicyTest(arguments []string) int { runResult, err := policytest.Run(policytest.RunOptions{ Policy: policy, Intent: intent, - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if err != nil { return writePolicyTestOutput(jsonOutput, policyTestOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) diff --git a/cmd/gait/registry.go b/cmd/gait/registry.go index e030bf01..175e4c5e 100644 --- a/cmd/gait/registry.go +++ b/cmd/gait/registry.go @@ -380,7 +380,7 @@ func writeRegistryVerificationReport(pathValue string, result registry.VerifyRes SchemaID: "gait.registry.verification_report", SchemaVersion: "1.0.0", CreatedAt: time.Date(1980, time.January, 1, 0, 0, 0, 0, time.UTC), - ProducerVersion: version, + ProducerVersion: currentVersion(), PackName: result.PackName, PackVersion: result.PackVersion, Digest: result.Digest, diff --git a/cmd/gait/regress.go b/cmd/gait/regress.go index d1a6c37a..0a5a451a 100644 --- a/cmd/gait/regress.go +++ b/cmd/gait/regress.go @@ -284,7 +284,7 @@ func runRegressRun(arguments []string) int { OutputPath: outputPath, JUnitPath: junitPath, WorkDir: ".", - ProducerVersion: version, + ProducerVersion: currentVersion(), AllowNondeterministic: allowNondeterministic, ContextConformance: contextConformance, AllowContextRuntimeDrift: allowContextRuntimeDrift, @@ -390,7 +390,7 @@ func runRegressBootstrap(arguments []string) int { OutputPath: outputPath, JUnitPath: junitPath, WorkDir: ".", - ProducerVersion: version, + ProducerVersion: currentVersion(), AllowNondeterministic: allowNondeterministic, ContextConformance: contextConformance, AllowContextRuntimeDrift: allowContextRuntimeDrift, diff --git a/cmd/gait/report.go b/cmd/gait/report.go index 8cd066d5..d34f5614 100644 --- a/cmd/gait/report.go +++ b/cmd/gait/report.go @@ -102,7 +102,7 @@ func runReportTop(arguments []string) int { TracePaths: tracePaths, Limit: limit, }, scout.TopActionsOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if err != nil { return writeReportTopOutput(jsonOutput, reportTopOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) diff --git a/cmd/gait/run_session.go b/cmd/gait/run_session.go index 297456ee..7b8f515d 100644 --- a/cmd/gait/run_session.go +++ b/cmd/gait/run_session.go @@ -98,7 +98,7 @@ func runSessionStart(arguments []string) int { status, err := runpack.StartSession(journal, runpack.SessionStartOptions{ SessionID: sessionID, RunID: runID, - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if err != nil { return writeRunSessionOutput(jsonOutput, runSessionOutput{ @@ -193,7 +193,7 @@ func runSessionAppend(arguments []string) int { } event, err := runpack.AppendSessionEvent(journal, runpack.SessionAppendOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), IntentID: intentID, ToolName: toolName, IntentDigest: intentDigest, @@ -327,7 +327,7 @@ func runSessionCheckpoint(arguments []string) int { } result, chainPath, err := runpack.SessionCheckpointAndWriteChain(journal, outPath, runpack.SessionCheckpointOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if err != nil { return writeRunSessionOutput(jsonOutput, runSessionOutput{ @@ -405,7 +405,7 @@ func runSessionCompact(arguments []string) int { } result, err := runpack.CompactSessionJournal(journal, runpack.SessionCompactionOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), OutputPath: outPath, DryRun: dryRun, }) diff --git a/cmd/gait/scout.go b/cmd/gait/scout.go index 50b58d0d..b11ae02f 100644 --- a/cmd/gait/scout.go +++ b/cmd/gait/scout.go @@ -108,7 +108,7 @@ func runScoutSnapshot(arguments []string) int { return writeScoutSnapshotOutput(jsonOutput, scoutSnapshotOutput{OK: false, Error: "unexpected positional arguments"}, exitInvalidInput) } - provider := scout.DefaultProvider{Options: scout.SnapshotOptions{ProducerVersion: version}} + provider := scout.DefaultProvider{Options: scout.SnapshotOptions{ProducerVersion: currentVersion()}} snapshot, err := provider.Snapshot(context.Background(), scout.SnapshotRequest{ Roots: parseCSVList(rootsCSV), Include: parseCSVList(includeCSV), @@ -278,7 +278,7 @@ func runScoutSignal(arguments []string) int { TracePaths: parseCSVList(tracesCSV), RegressPaths: parseCSVList(regressCSV), }, scout.SignalOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if err != nil { return writeScoutSignalOutput(jsonOutput, scoutSignalOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) diff --git a/cmd/gait/tour.go b/cmd/gait/tour.go index 6d75d10b..8304a9ee 100644 --- a/cmd/gait/tour.go +++ b/cmd/gait/tour.go @@ -97,7 +97,7 @@ func runTour(arguments []string) int { OutputPath: regressOutputPath, JUnitPath: regressJUnitPath, WorkDir: ".", - ProducerVersion: version, + ProducerVersion: currentVersion(), }) if err != nil { return writeTourOutput(jsonOutput, tourOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) diff --git a/cmd/gait/version_resolver.go b/cmd/gait/version_resolver.go new file mode 100644 index 00000000..a3974cfb --- /dev/null +++ b/cmd/gait/version_resolver.go @@ -0,0 +1,128 @@ +package main + +import ( + "regexp" + "runtime/debug" + "strings" + "unicode" +) + +const localDevVersion = "0.0.0-dev" + +var readBuildInfo = debug.ReadBuildInfo + +var ( + semverLikeVersionPattern = regexp.MustCompile(`^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$`) + baseVersionPattern = regexp.MustCompile(`^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$`) + revisionPattern = regexp.MustCompile(`^[0-9a-f]{12,}$`) +) + +func currentVersion() string { + return resolveCLIVersion(version, readBuildInfo) +} + +func resolveCLIVersion(stamped string, reader func() (*debug.BuildInfo, bool)) string { + if candidate := normalizeExplicitVersion(stamped); candidate != "" && candidate != localDevVersion { + return candidate + } + if reader != nil { + if info, ok := reader(); ok { + if candidate := trustedBuildInfoVersion(info); candidate != "" { + return candidate + } + } + } + if candidate := normalizeExplicitVersion(stamped); candidate != "" { + return candidate + } + return localDevVersion +} + +func normalizeExplicitVersion(candidate string) string { + trimmed := strings.TrimSpace(candidate) + if trimmed == "" { + return "" + } + if normalized, ok := normalizeReleaseVersion(trimmed); ok { + return normalized + } + return trimmed +} + +func trustedBuildInfoVersion(info *debug.BuildInfo) string { + if info == nil { + return "" + } + candidate := strings.TrimSpace(info.Main.Version) + if candidate == "" || candidate == "(devel)" || strings.Contains(candidate, "+dirty") { + return "" + } + if !semverLikeVersionPattern.MatchString(candidate) || isPseudoVersion(candidate) { + return "" + } + normalized, ok := normalizeReleaseVersion(candidate) + if !ok { + return "" + } + return normalized +} + +func normalizeReleaseVersion(candidate string) (string, bool) { + trimmed := strings.TrimSpace(candidate) + if trimmed == localDevVersion { + return "", false + } + if !semverLikeVersionPattern.MatchString(trimmed) { + return "", false + } + return strings.TrimPrefix(trimmed, "v"), true +} + +func isPseudoVersion(candidate string) bool { + core := strings.TrimSpace(candidate) + if buildIndex := strings.IndexByte(core, '+'); buildIndex >= 0 { + core = core[:buildIndex] + } + + lastDash := strings.LastIndexByte(core, '-') + if lastDash <= 0 || lastDash == len(core)-1 { + return false + } + revision := core[lastDash+1:] + if !revisionPattern.MatchString(revision) { + return false + } + + prefix := core[:lastDash] + for _, separator := range []string{"-0.", ".0.", "-"} { + timestampStart := len(prefix) - 14 + if timestampStart <= len(separator)-1 { + continue + } + if prefix[timestampStart-len(separator):timestampStart] != separator { + continue + } + timestamp := prefix[timestampStart:] + if !isAllDigits(timestamp) { + continue + } + base := prefix[:timestampStart-len(separator)] + if baseVersionPattern.MatchString(base) { + return true + } + } + + return false +} + +func isAllDigits(value string) bool { + if value == "" { + return false + } + for _, r := range value { + if !unicode.IsDigit(r) { + return false + } + } + return true +} diff --git a/cmd/gait/version_resolver_test.go b/cmd/gait/version_resolver_test.go new file mode 100644 index 00000000..67cf1ff4 --- /dev/null +++ b/cmd/gait/version_resolver_test.go @@ -0,0 +1,82 @@ +package main + +import ( + "runtime/debug" + "testing" +) + +func TestResolveCLIVersionPrefersStampedRelease(t *testing.T) { + got := resolveCLIVersion("1.2.3", func() (*debug.BuildInfo, bool) { + return &debug.BuildInfo{ + Main: debug.Module{Version: "v9.9.9"}, + }, true + }) + if got != "1.2.3" { + t.Fatalf("resolveCLIVersion stamped release: got %q want %q", got, "1.2.3") + } +} + +func TestResolveCLIVersionUsesTrustedBuildInfoRelease(t *testing.T) { + got := resolveCLIVersion(localDevVersion, func() (*debug.BuildInfo, bool) { + return &debug.BuildInfo{ + Main: debug.Module{Version: "v1.3.5"}, + }, true + }) + if got != "1.3.5" { + t.Fatalf("resolveCLIVersion build info release: got %q want %q", got, "1.3.5") + } +} + +func TestResolveCLIVersionIgnoresUntrustedBuildInfo(t *testing.T) { + testCases := []struct { + name string + version string + }{ + {name: "devel", version: "(devel)"}, + {name: "pseudo release bump", version: "v1.3.6-0.20260318215907-c90958e0d34e"}, + {name: "pseudo no prior tag", version: "v1.0.0-20260318215907-c90958e0d34e"}, + {name: "pseudo prerelease", version: "v1.3.5-rc.1.0.20260318215907-c90958e0d34e"}, + {name: "dirty", version: "v1.3.6-0.20260318215907-c90958e0d34e+dirty"}, + {name: "empty", version: ""}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := resolveCLIVersion(localDevVersion, func() (*debug.BuildInfo, bool) { + return &debug.BuildInfo{ + Main: debug.Module{Version: testCase.version}, + }, true + }) + if got != localDevVersion { + t.Fatalf("resolveCLIVersion(%q): got %q want %q", testCase.version, got, localDevVersion) + } + }) + } +} + +func TestNormalizeReleaseVersion(t *testing.T) { + testCases := []struct { + name string + input string + expected string + normalize bool + }{ + {name: "plain semver", input: "1.2.3", expected: "1.2.3", normalize: true}, + {name: "tagged semver", input: "v1.2.3", expected: "1.2.3", normalize: true}, + {name: "prerelease", input: "v1.2.3-rc.1", expected: "1.2.3-rc.1", normalize: true}, + {name: "dev", input: localDevVersion, normalize: false}, + {name: "custom", input: "feature-branch", normalize: false}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got, ok := normalizeReleaseVersion(testCase.input) + if ok != testCase.normalize { + t.Fatalf("normalizeReleaseVersion(%q) ok=%t want %t", testCase.input, ok, testCase.normalize) + } + if got != testCase.expected { + t.Fatalf("normalizeReleaseVersion(%q) got %q want %q", testCase.input, got, testCase.expected) + } + }) + } +} diff --git a/cmd/gait/voice.go b/cmd/gait/voice.go index d4e06475..088371ef 100644 --- a/cmd/gait/voice.go +++ b/cmd/gait/voice.go @@ -158,7 +158,7 @@ func runVoiceTokenMint(arguments []string) int { if err != nil { return writeVoiceTokenOutput(jsonOutput, voiceTokenOutput{OK: false, Operation: "mint", Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) } - outcome, err := gate.EvaluatePolicyDetailed(policy, normalizedIntent, gate.EvalOptions{ProducerVersion: version}) + outcome, err := gate.EvaluatePolicyDetailed(policy, normalizedIntent, gate.EvalOptions{ProducerVersion: currentVersion()}) if err != nil { return writeVoiceTokenOutput(jsonOutput, voiceTokenOutput{OK: false, Operation: "mint", Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) } @@ -175,7 +175,7 @@ func runVoiceTokenMint(arguments []string) int { return writeVoiceTokenOutput(jsonOutput, voiceTokenOutput{OK: false, Operation: "mint", Error: err.Error()}, exitCodeForError(err, exitInvalidInput)) } traceResult, traceErr := gate.EmitSignedTrace(policy, normalizedIntent, outcome.Result, gate.EmitTraceOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), CorrelationID: currentCorrelationID(), ContextSource: outcome.ContextSource, CompositeRiskClass: outcome.CompositeRiskClass, @@ -212,7 +212,7 @@ func runVoiceTokenMint(arguments []string) int { }, exitCode) } tokenResult, err := gate.MintSayToken(gate.MintSayTokenOptions{ - ProducerVersion: version, + ProducerVersion: currentVersion(), CommitmentClass: normalizedCommitment.CommitmentClass, IntentDigest: intentDigest, PolicyDigest: policyDigest, diff --git a/docs-site/public/llm/contracts.md b/docs-site/public/llm/contracts.md index 5673df1b..59095905 100644 --- a/docs-site/public/llm/contracts.md +++ b/docs-site/public/llm/contracts.md @@ -12,6 +12,7 @@ Stable OSS contracts include: - **Python SDK Demo Contract**: machine-readable SDK/demo capture consumes `gait demo --json` output only; the human text form is non-contractual. - `run_session(...)` delegates digest-bearing runpack fields to `gait run record` in Go rather than hashing them in Python. - unsupported `set` values and other non-JSON payloads are rejected deterministically. + - `sdk/python` version metadata is repo-local dev metadata; release/install verification uses `gait version --json`. - **Doctor Install Contract**: `gait doctor --json` is truthful for a clean writable binary-install lane, returning `status=pass|warn` there and only surfacing repo-only checks from a Gait repo checkout. - **Repo Policy Contract**: `gait init` writes `.gait.yaml` and returns `detected_signals`, `generated_rules`, and `unknown_signals`; `gait check` reports the live contract with `default_verdict`, `rule_count`, structured `findings`, compatibility `gap_warnings`, and install-safe `next_commands`. - **Draft Proposal Migration Contract**: keep the shipped policy DSL (`schema_id`, `schema_version`, `default_verdict`, optional `fail_closed`, optional `mcp_trust`, `rules`); proposal keys like `version`, `name`, `boundaries`, `defaults`, `trust_sources`, and `unknown_server` return deterministic migration guidance instead of enabling a second DSL. diff --git a/docs-site/public/llm/faq.md b/docs-site/public/llm/faq.md index d377d806..d50d1ffc 100644 --- a/docs-site/public/llm/faq.md +++ b/docs-site/public/llm/faq.md @@ -52,7 +52,7 @@ Yes. `gait run replay` uses recorded results as deterministic stubs so you can d Gait provides wrapper or sidecar, Python SDK, and MCP boundary modes. The official LangChain surface is middleware with optional callback correlation; enforcement still happens only at the tool boundary. Claude Code remains a reference adapter, and its hook/runtime/input errors fail closed by default unless an operator explicitly opts into unsafe fail-open behavior. -Official lanes today are OpenAI Agents and LangChain. Reference adapters stay in-repo, but they are not promoted into official launch claims until they clear the scorecard threshold. CrewAI is not an official lane today. +The in-repo OpenAI Agents path is a reference boundary demo. LangChain is the official framework lane today. Reference adapters stay in-repo, but they are not promoted into official launch claims until they clear the scorecard threshold. CrewAI is not an official lane today. If you use `gait test`, `gait enforce`, or `gait trace`, the child integration must emit a `trace_path=` seam. Wrapper JSON makes that explicit with `boundary_contract=explicit_trace_reference`, `trace_reference_required=true`, and stable `failure_reason` values such as `missing_trace_reference` or `invalid_trace_artifact`. diff --git a/docs-site/public/llm/product.md b/docs-site/public/llm/product.md index 34d48be6..379effe1 100644 --- a/docs-site/public/llm/product.md +++ b/docs-site/public/llm/product.md @@ -19,14 +19,14 @@ Secondary boundary surfaces: - **MCP Trust**: evaluate local trust snapshots for MCP server admission with `gait mcp verify`, `gait mcp proxy`, and `gait mcp serve`. - **Trace**: observe-only wrapper mode with `gait trace` for integrations that already emit Gait trace references. - **LangChain Middleware**: official Python middleware with optional callback correlation; callbacks never decide allow or block behavior, and demo capture stays bound to `gait demo --json`. +- **OpenAI Agents Reference Demo**: in-repo boundary demo showing the wrapper contract with deterministic allow, block, and approval outcomes. It is not a package-backed official SDK lane. - **Reference Adapters**: `examples/integrations/claude_code/` remains a reference lane; its hook/runtime/input errors fail closed by default and `GAIT_CLAUDE_UNSAFE_FAIL_OPEN=1` is an unsafe opt-in override. -Official framework lanes today: +Official framework lane today: -- OpenAI Agents - LangChain middleware -Reference adapters stay in-repo but outside official-lane claims until they clear the promotion scorecard. CrewAI is not an official lane today. +The OpenAI Agents path in this repo is a reference boundary demo. Reference adapters stay in-repo but outside official-lane claims until they clear the promotion scorecard. CrewAI is not an official lane today. Gait is vendor-neutral and offline-first for core workflows: capture, verify, diff, policy evaluation, regressions, and voice/context verification all run without network dependencies. diff --git a/docs-site/public/llm/quickstart.md b/docs-site/public/llm/quickstart.md index ff349afc..4c07bb85 100644 --- a/docs-site/public/llm/quickstart.md +++ b/docs-site/public/llm/quickstart.md @@ -82,7 +82,7 @@ Do not treat `oss-prod` enforcement as production-ready until that doctor comman Standard `gait doctor --json` is truthful in a clean writable directory after a binary-only install: repo-only schema/example checks stay scoped to a Gait repo checkout. -Wrapper lane example: +Reference boundary demo: ```bash gait test --json -- python3 examples/integrations/openai_agents/quickstart.py --scenario allow @@ -91,6 +91,8 @@ gait capture --from run_demo --json gait regress add --from ./gait-out/capture.json --json ``` +The in-repo OpenAI Agents example is a reference boundary demo, not a package-backed official SDK lane. LangChain remains the official middleware lane. + For MCP server admission, keep trust inputs local: ```bash diff --git a/docs-site/public/llms.txt b/docs-site/public/llms.txt index 24db818f..1c43df31 100644 --- a/docs-site/public/llms.txt +++ b/docs-site/public/llms.txt @@ -42,7 +42,7 @@ - Context-required policies must receive `--context-envelope ` at that boundary; raw context digest/mode/age claims are not authoritative alone. - For `gait mcp serve`, same-host callers may use `call.context.context_envelope_path` only when the server explicitly enables `--allow-client-artifact-paths`. - Official LangChain lane: middleware with optional callback correlation, not a separate policy engine. -- Official framework lanes today: OpenAI Agents and LangChain middleware. +- OpenAI Agents in this repo is a reference boundary demo, not a package-backed official SDK lane. - Reference adapters stay in-repo but outside official-lane claims until they clear the promotion scorecard threshold. CrewAI is not an official lane today. ## When To Use @@ -59,6 +59,7 @@ - Fail-closed by default for ambiguous high-risk policy outcomes. - Equal-priority rule overlaps resolve to the most restrictive verdict for that priority tier, not by rule name ordering. - `gait --help` is text-only and exits `0`; machine consumers should use `gait version --json`, `gait --version --json`, or `gait -v --json`. +- `sdk/python` keeps repo-local dev metadata (`0.0.0.dev0`); release/install automation should use `gait version --json`. - Machine-readable wrappers and SDKs use `gait demo --json`; the text form is human-facing only. - Python run-session capture delegates digest-bearing artifact fields to `gait run record` in Go; convert `set` values to JSON lists before calling the SDK. - `examples/integrations/claude_code/` is a reference adapter; hook/runtime/input errors fail closed by default and `GAIT_CLAUDE_UNSAFE_FAIL_OPEN=1` is an unsafe opt-in override. diff --git a/docs/README.md b/docs/README.md index 208d3aef..2248726f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ Extended first-class surfaces: 1. `README.md` for the product wedge and first five commands 2. `docs/policy_authoring.md` for `.gait.yaml`, validation, and rollout workflow -3. `docs/integration_checklist.md` for official OpenAI Agents and LangChain lanes plus the reference-adapter promotion rules +3. `docs/integration_checklist.md` for the official LangChain lane, the OpenAI reference demo, and the reference-adapter promotion rules 4. `docs/agent_integration_boundary.md` for tool-boundary placement rules 5. `docs/install.md` for the truthful binary-install lane (`doctor`, `init`, `check`, `demo`, `verify`) 6. `docs/ci_regress_kit.md` for incident-to-CI adoption @@ -91,7 +91,7 @@ Extended first-class surfaces: - Ecosystem contribution flow: `docs/ecosystem/contribute.md` - Launch/distribution assets: `docs/launch/README.md` - Activation KPI definition (v2.6): `docs/launch/activation_kpi_v2_6.md` -- Distribution plan (v2.7 gravity wells): `docs/PLAN_v2.7_distribution.md` +- Internal distribution planning artifact: `docs/PLAN_v2.7_distribution.md` - Marketplace action publishing path: `docs/marketplace_action_publishing.md` - Canonical MCP boundary demo: `docs/scenarios/mcp_canonical_boundary.md` - Content cadence plan (v2.6): `docs/launch/content_cadence_v2_6.md` diff --git a/docs/agent_integration_boundary.md b/docs/agent_integration_boundary.md index 0f92f842..2daf1d2f 100644 --- a/docs/agent_integration_boundary.md +++ b/docs/agent_integration_boundary.md @@ -91,7 +91,7 @@ What Gait cannot do (without interception): ## Practical Paths -- Tier A quickstart: `examples/integrations/openai_agents/quickstart.py` +- Tier A reference demo quickstart: `examples/integrations/openai_agents/quickstart.py` - Tier A/B transport path: `gait mcp serve --context-envelope ./context_envelope.json` - Reference adapter note: `examples/integrations/claude_code/gait-gate.sh` fails closed on hook/runtime/input errors by default; `GAIT_CLAUDE_UNSAFE_FAIL_OPEN=1` is a debugging-only unsafe override, not a promoted lane. - Tier C fallback: `gait report top`, `gait capture`, `gait regress add`, `gait regress run` (or `gait regress bootstrap` for the one-command path) diff --git a/docs/ci_regress_kit.md b/docs/ci_regress_kit.md index 46f1542e..60498e11 100644 --- a/docs/ci_regress_kit.md +++ b/docs/ci_regress_kit.md @@ -7,7 +7,7 @@ description: "Wire Gait regression fixtures into GitHub Actions, GitLab CI, Jenk This kit keeps CI adoption "one PR to adopt" while preserving deterministic regress contracts. -Version note: this page is evergreen. Release-specific packaging rollout notes belong in `docs/PLAN_v2.7_distribution.md` and release notes. +Version note: this page is evergreen. Release-specific rollout notes belong in changelog and internal plan documents, not here. ## One-PR Adoption Path (Default) diff --git a/docs/contracts/compatibility_matrix.md b/docs/contracts/compatibility_matrix.md index 12a66ef5..f016fdf8 100644 --- a/docs/contracts/compatibility_matrix.md +++ b/docs/contracts/compatibility_matrix.md @@ -11,16 +11,13 @@ This matrix defines compatibility between producer and consumer surfaces. - This page is the contract-level source for version compatibility. - Evergreen operational docs should not carry release tags in titles. -- Release-lane context belongs in release notes and plan documents (for example `docs/PLAN_v2.7_distribution.md`). +- Internal rollout labels belong in changelog and plan documents, not evergreen compatibility copy. ## Version Matrix | Gait CLI | PackSpec | `gait pack verify` behavior | Legacy runpack verify via `pack verify` | | --- | --- | --- | --- | -| v2.4.x | 1.0.0 | verifies PackSpec v1 (`run`, `job`) | supported | -| v2.5.x | 1.0.0 | verifies PackSpec v1 (`run`, `job`, `call`) | supported | -| v2.6.x | 1.0.0 | verifies PackSpec v1 (`run`, `job`, `call`) + context-aware diff metadata | supported | -| v2.7.x | 1.0.0 | same as v2.6 with CI/template hardening updates | supported | +| current `1.x` release line | 1.0.0 | verifies PackSpec v1 (`run`, `job`, `call`) with additive verifier hardening and context-aware diff metadata where applicable | supported | ## Stability Guarantees diff --git a/docs/ecosystem/awesome.md b/docs/ecosystem/awesome.md index c5ce3bf0..d6aa859d 100644 --- a/docs/ecosystem/awesome.md +++ b/docs/ecosystem/awesome.md @@ -11,15 +11,15 @@ Validation: - `python3 scripts/validate_community_index.py` - `python3 scripts/render_ecosystem_release_notes.py` -## Official Integrations +## Maintained In-Repo Integrations -- OpenAI Agents: `examples/integrations/openai_agents/` -- LangChain: `examples/integrations/langchain/` -- AutoGen: `examples/integrations/autogen/` -- OpenClaw: `examples/integrations/openclaw/` +- LangChain (official middleware lane): `examples/integrations/langchain/` +- OpenAI Agents reference boundary demo: `examples/integrations/openai_agents/` +- AutoGen reference adapter: `examples/integrations/autogen/` +- OpenClaw reference adapter: `examples/integrations/openclaw/` - OpenClaw installable skill package: `examples/integrations/openclaw/skill/` -- AutoGPT: `examples/integrations/autogpt/` -- Gas Town: `examples/integrations/gastown/` +- AutoGPT reference adapter: `examples/integrations/autogpt/` +- Gas Town reference adapter: `examples/integrations/gastown/` ## Official Skills @@ -33,6 +33,6 @@ Validation: - Every adapter entry must pass `bash scripts/test_adapter_parity.sh` behavior. - Every skill entry must declare provenance and avoid direct policy logic in non-Go layers. - Every entry must include a public GitHub repo URL and a stable summary. -- Official lane expansion in v2.3 requires scorecard evidence (`scripts/check_integration_lane_scorecard.py`) meeting threshold + confidence gates. +- Maintainer-promoted official-lane expansion requires scorecard evidence (`scripts/check_integration_lane_scorecard.py`) meeting threshold + confidence gates. See `docs/ecosystem/contribute.md` for the full submission workflow. diff --git a/docs/ecosystem/community_index.json b/docs/ecosystem/community_index.json index e3a4917c..0a839acb 100644 --- a/docs/ecosystem/community_index.json +++ b/docs/ecosystem/community_index.json @@ -47,7 +47,7 @@ "id": "adapter-openai-agents-official", "kind": "adapter", "name": "OpenAI Agents Adapter Example", - "summary": "Reference OpenAI Agents integration preserving no-bypass execution-boundary controls.", + "summary": "Reference OpenAI-style boundary demo preserving no-bypass execution-boundary controls; not a package-backed official SDK lane.", "repo": "https://github.com/Clyra-AI/gait", "source": "official", "status": "stable", diff --git a/docs/ecosystem/contribute.md b/docs/ecosystem/contribute.md index 29f0f3b8..5e11500a 100644 --- a/docs/ecosystem/contribute.md +++ b/docs/ecosystem/contribute.md @@ -14,11 +14,13 @@ Required in proposal: - fail-closed behavior (`allow` vs non-`allow` execution) - deterministic output paths + test plan -## 2) Respect v2.3 Lane Governance +## 2) Respect Current Lane Governance -Blessed lane in v2.3: +Default starter surfaces: -- coding-agent wrapper + GitHub Actions regress template +- official framework lane: LangChain middleware +- reference boundary demo: `examples/integrations/openai_agents/` +- GitHub Actions regress template No new official lane is merged without scorecard evidence: diff --git a/docs/integration_checklist.md b/docs/integration_checklist.md index 9931af00..92af4075 100644 --- a/docs/integration_checklist.md +++ b/docs/integration_checklist.md @@ -1,6 +1,6 @@ --- title: "Integration Checklist" -description: "Step-by-step checklist for integrating Gait at the tool boundary with the shipped OpenAI Agents lane, official LangChain middleware, and reference adapters." +description: "Step-by-step checklist for integrating Gait at the tool boundary with the OpenAI reference demo, official LangChain middleware, and reference adapters." --- # Gait Integration Checklist @@ -22,9 +22,13 @@ This checklist is evergreen guidance. Release-specific rollouts belong in plan/c ## Lane Governance (Locked) -Blessed default lane: +Official framework lane: -- local coding-agent wrapper flow (`examples/integrations/openai_agents/`) +- LangChain middleware (`examples/integrations/langchain/`) + +Reference starter surfaces: + +- local coding-agent wrapper demo (`examples/integrations/openai_agents/`) - GitHub Actions CI regress gate (`.github/workflows/adoption-regress-template.yml`) - one-PR CI adoption target via reusable workflow or composite action @@ -196,13 +200,13 @@ Contract docs: ### Adapter Parity / Secondary Lanes -Official lanes: +Official lane: -- `examples/integrations/openai_agents/` - `examples/integrations/langchain/` (official middleware with optional callback correlation) -Reference adapters: +Reference demos and adapters: +- `examples/integrations/openai_agents/` (reference boundary demo) - `examples/integrations/autogen/` - `examples/integrations/openclaw/` - `examples/integrations/autogpt/` @@ -228,7 +232,7 @@ bash scripts/test_adoption_smoke.sh 5. Emit runpack 6. Convert runpack into regress fixture -### Minimal Commands (Blessed Path) +### Minimal Commands (Reference + Official Paths) ```bash go build -o ./gait ./cmd/gait @@ -309,7 +313,7 @@ If your agent runtime is fully hosted with no interception point, Gait can still ### Which integration path should I start with? -The blessed lane is OpenAI Agents (`examples/integrations/openai_agents/`). LangChain is the official middleware lane. Other adapters are reference parity lanes on the same contract. +Start with the OpenAI reference demo when you want the fastest local boundary proof, or start with LangChain when you want the official middleware lane. Other adapters are reference parity lanes on the same contract. ### Do I need to modify my agent code? diff --git a/docs/scenarios/simple_agent_tool_boundary.md b/docs/scenarios/simple_agent_tool_boundary.md index 9c3bf720..326a2e08 100644 --- a/docs/scenarios/simple_agent_tool_boundary.md +++ b/docs/scenarios/simple_agent_tool_boundary.md @@ -25,7 +25,7 @@ A tool boundary is the exact call site where your runtime is about to execute a Where to inspect this in code: -- wrapper flow: `examples/integrations/openai_agents/quickstart.py` +- reference wrapper flow: `examples/integrations/openai_agents/quickstart.py` - gate command wiring: `cmd/gait/gate.go` - policy evaluation engine: `core/gate/` diff --git a/docs/sdk/python.md b/docs/sdk/python.md index fe94c317..d6eb7179 100644 --- a/docs/sdk/python.md +++ b/docs/sdk/python.md @@ -114,6 +114,10 @@ LangChain is opt-in: Python 3.11 or higher. +### Should I use `gait.__version__` as the install probe? + +No. `sdk/python` keeps repo-local dev metadata (`0.0.0.dev0`) and is not the public release signal. Use `gait version --json` for install automation, support intake, and release verification. + ### How do I wrap a tool function with Gait? Use the `@gate_tool` decorator from the SDK. It automatically evaluates gate policy before executing the tool and records the result. diff --git a/docs/threat_model.md b/docs/threat_model.md index 7580b580..a90d6e99 100644 --- a/docs/threat_model.md +++ b/docs/threat_model.md @@ -5,7 +5,7 @@ description: "Baseline OSS threat model for Gait runtime boundaries, artifact in # Gait Threat Model -Status: baseline threat model for current OSS release line (`v2.7.x`). +Status: baseline threat model for the current OSS `1.x` release line. This document defines what Gait protects, where trust boundaries are, and which controls are expected in production deployment. diff --git a/examples/integrations/README.md b/examples/integrations/README.md index d2bf43c2..ef799a50 100644 --- a/examples/integrations/README.md +++ b/examples/integrations/README.md @@ -2,11 +2,14 @@ This directory contains framework adapters that demonstrate the same Gait execution contract across runtimes. -Official lanes: +Official lane: -- `openai_agents` - `langchain` (official middleware with optional callback correlation) +Reference boundary demo: + +- `openai_agents` + Quick commands: ```bash diff --git a/examples/integrations/openai_agents/README.md b/examples/integrations/openai_agents/README.md index 8527de7f..a7da874a 100644 --- a/examples/integrations/openai_agents/README.md +++ b/examples/integrations/openai_agents/README.md @@ -1,11 +1,11 @@ -# OpenAI Agents Quickstart (Blessed v2.3 Lane) +# OpenAI Agents Reference Boundary Demo -This is the top-of-funnel integration lane for v2.3: +This is the fastest in-repo reference demo for the boundary contract: - local wrapper enforcement for OpenAI-style tool calls - direct mapping to GitHub Actions regress gate -Use this when you want the clearest runtime-boundary example of where Gait sits between agent and tool execution. +Use this when you want the clearest runtime-boundary example of where Gait sits between agent and tool execution. It is not a package-backed official SDK lane. Related onboarding: diff --git a/examples/integrations/openai_agents/quickstart.py b/examples/integrations/openai_agents/quickstart.py index 4bfa4a0f..ad636ca0 100755 --- a/examples/integrations/openai_agents/quickstart.py +++ b/examples/integrations/openai_agents/quickstart.py @@ -146,7 +146,7 @@ def execute_wrapped_tool(intent_payload: dict[str, Any], output_path: Path) -> N def main() -> int: parser = argparse.ArgumentParser( - description="OpenAI Agents wrapped tool-call quickstart" + description="OpenAI-style reference boundary demo" ) parser.add_argument( "--scenario", diff --git a/scripts/test_cli_version_install_paths.sh b/scripts/test_cli_version_install_paths.sh index c383e437..f26f8840 100755 --- a/scripts/test_cli_version_install_paths.sh +++ b/scripts/test_cli_version_install_paths.sh @@ -225,4 +225,26 @@ else assert_version "brew-install" "${brew_prefix}/bin/gait-local" "${release_version}" fi +echo "==> python sdk metadata stays dev-only" +python3 - <<'PY' "${REPO_ROOT}/sdk/python/pyproject.toml" "${REPO_ROOT}/sdk/python/gait/__init__.py" +import re +import sys +import tomllib +from pathlib import Path + +pyproject = Path(sys.argv[1]) +init_py = Path(sys.argv[2]) +project = tomllib.loads(pyproject.read_text(encoding="utf-8"))["project"] +sdk_version = project["version"] +description = project["description"] +match = re.search(r'^__version__\s*=\s*"([^"]+)"', init_py.read_text(encoding="utf-8"), re.MULTILINE) +if not match: + raise SystemExit("__version__ not found in sdk/python/gait/__init__.py") +init_version = match.group(1) +if sdk_version != "0.0.0.dev0" or init_version != "0.0.0.dev0": + raise SystemExit(f"expected repo-local dev sdk version 0.0.0.dev0, got pyproject={sdk_version} init={init_version}") +if "repo-local dev" not in description: + raise SystemExit(f"expected repo-local dev description, got: {description}") +PY + echo "install-path version smoke: pass" diff --git a/scripts/test_install.sh b/scripts/test_install.sh index 1b757101..7225c7cc 100755 --- a/scripts/test_install.sh +++ b/scripts/test_install.sh @@ -87,6 +87,43 @@ fi export PATH="${install_dir}:$PATH" +echo "==> installed binary version probe" +SOURCE_VERSION_JSON="$("${BIN_PATH}" version --json)" +INSTALLED_VERSION_JSON="$("${install_dir}/gait" version --json)" +ALIAS_VERSION_JSON="$("${install_dir}/gait" --version --json)" +SHORT_VERSION_JSON="$("${install_dir}/gait" -v --json)" + +SOURCE_VERSION_JSON="${SOURCE_VERSION_JSON}" \ +INSTALLED_VERSION_JSON="${INSTALLED_VERSION_JSON}" \ +ALIAS_VERSION_JSON="${ALIAS_VERSION_JSON}" \ +SHORT_VERSION_JSON="${SHORT_VERSION_JSON}" \ +python3 - <<'PY' +import json +import os + +source = json.loads(os.environ["SOURCE_VERSION_JSON"]) +installed = json.loads(os.environ["INSTALLED_VERSION_JSON"]) +alias = json.loads(os.environ["ALIAS_VERSION_JSON"]) +short = json.loads(os.environ["SHORT_VERSION_JSON"]) + +for name, payload in { + "source": source, + "installed": installed, + "alias": alias, + "short": short, +}.items(): + if payload.get("ok") is not True: + raise SystemExit(f"{name} version probe expected ok=true, got {payload}") + version = payload.get("version") + if not isinstance(version, str) or not version: + raise SystemExit(f"{name} version probe missing version: {payload}") + +expected = source["version"] +for name, payload in {"installed": installed, "alias": alias, "short": short}.items(): + if payload["version"] != expected: + raise SystemExit(f"{name} version {payload['version']} != source version {expected}") +PY + bash "${REPO_ROOT}/scripts/test_onboarding_contract.sh" "${install_dir}/gait" "${work_dir}/onboarding" "${install_dir}/gait" demo > "${work_dir}/demo.txt" diff --git a/scripts/test_release_smoke.sh b/scripts/test_release_smoke.sh index 513f9162..717b4455 100644 --- a/scripts/test_release_smoke.sh +++ b/scripts/test_release_smoke.sh @@ -35,6 +35,32 @@ bash "$REPO_ROOT/scripts/test_onboarding_contract.sh" "$BIN_PATH" "$WORK_DIR/onb cd "$WORK_DIR" +echo "==> version probe aliases" +"$BIN_PATH" version --json > version.json +"$BIN_PATH" --version --json > version_alias.json +"$BIN_PATH" -v --json > version_short.json + +python3 - <<'PY' +import json +from pathlib import Path + +version = json.loads(Path("version.json").read_text(encoding="utf-8")) +alias = json.loads(Path("version_alias.json").read_text(encoding="utf-8")) +short = json.loads(Path("version_short.json").read_text(encoding="utf-8")) + +for name, payload in {"version": version, "alias": alias, "short": short}.items(): + if payload.get("ok") is not True: + raise SystemExit(f"{name} probe expected ok=true, got {payload}") + if not isinstance(payload.get("version"), str) or not payload["version"]: + raise SystemExit(f"{name} probe missing version: {payload}") + +expected = version["version"] +if alias["version"] != expected or short["version"] != expected: + raise SystemExit( + f"version probe mismatch: version={expected} alias={alias['version']} short={short['version']}" + ) +PY + echo "==> demo -> verify" "$BIN_PATH" demo > demo.txt grep -q '^run_id=run_demo$' demo.txt diff --git a/scripts/test_uat_local.sh b/scripts/test_uat_local.sh index 46c5e115..25044a5f 100755 --- a/scripts/test_uat_local.sh +++ b/scripts/test_uat_local.sh @@ -212,6 +212,8 @@ else fi log "Requested install-path suite mode for release/brew: ${INSTALL_PATH_MODE_REQUESTED}" +rm -rf "${REPO_ROOT}/docs-site/node_modules" + run_step "quality_lint" make -C "${REPO_ROOT}" lint run_step "quality_test" make -C "${REPO_ROOT}" test run_step "quality_e2e" make -C "${REPO_ROOT}" test-e2e @@ -247,7 +249,7 @@ fi if [[ "${SKIP_DOCS_SITE}" == "true" ]]; then log "SKIP quality_docs_site (requested)" elif command -v npm >/dev/null 2>&1; then - run_step "quality_docs_site" make -C "${REPO_ROOT}" docs-site-lint docs-site-build docs-site-check + run_step "quality_docs_site" make -C "${REPO_ROOT}" docs-site-validate else log "SKIP quality_docs_site (npm missing)" fi diff --git a/sdk/python/gait/__init__.py b/sdk/python/gait/__init__.py index a023e155..ffdabc5d 100644 --- a/sdk/python/gait/__init__.py +++ b/sdk/python/gait/__init__.py @@ -64,4 +64,6 @@ "write_trace", ] +# Repo-local development metadata only; use `gait version --json` as the +# authoritative install/version probe for release binaries and support intake. __version__ = "0.0.0.dev0" diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index e59abecd..a2955b4a 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "gait" version = "0.0.0.dev0" -description = "Gait Python SDK (thin wrapper)" +description = "Gait Python SDK (repo-local dev thin wrapper)" requires-python = ">=3.11" dependencies = [] diff --git a/sdk/python/tests/test_basic.py b/sdk/python/tests/test_basic.py index cf081875..ddda4679 100644 --- a/sdk/python/tests/test_basic.py +++ b/sdk/python/tests/test_basic.py @@ -1,4 +1,4 @@ def test_import() -> None: import gait - assert gait.__version__ + assert gait.__version__ == "0.0.0.dev0" diff --git a/sdk/python/tests/test_client.py b/sdk/python/tests/test_client.py index 54e4fced..a6abc679 100644 --- a/sdk/python/tests/test_client.py +++ b/sdk/python/tests/test_client.py @@ -284,6 +284,96 @@ def test_evaluate_gate_with_all_optional_key_flags(tmp_path: Path) -> None: assert result.exit_code == 0 +def test_evaluate_gate_with_delegation_and_delegation_key_flags( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + observed_commands: list[list[str]] = [] + + def fake_run(command: Sequence[str], cwd: object = None) -> client_module._CommandResult: + observed_commands.append(list(command)) + return client_module._CommandResult( + command=list(command), + exit_code=0, + stdout=json.dumps( + { + "ok": True, + "verdict": "allow", + "reason_codes": ["default_allow"], + "trace_id": "trace_1", + "trace_path": "trace.json", + "policy_digest": "p" * 64, + "intent_digest": "i" * 64, + } + ), + stderr="", + ) + + intent = capture_intent( + tool_name="tool.allow", + args={"path": "/tmp/out.txt"}, + context=IntentContext(identity="alice", workspace="/repo/gait", risk_class="high"), + ) + + with pytest.MonkeyPatch.context() as patch: + patch.setattr(client_module, "_run_command", fake_run) + result = evaluate_gate( + policy_path=tmp_path / "policy.yaml", + intent=intent, + gait_bin="gait", + cwd=tmp_path, + delegation_token=tmp_path / "delegation.json", + delegation_token_chain=[tmp_path / "delegation-a.json", tmp_path / "delegation-b.json"], + delegation_public_key=tmp_path / "delegation.pub", + delegation_public_key_env="GAIT_DELEGATION_PUBLIC_KEY", + delegation_private_key=tmp_path / "delegation.key", + delegation_private_key_env="GAIT_DELEGATION_PRIVATE_KEY", + ) + + assert result.ok + assert len(observed_commands) == 1 + command = observed_commands[0] + assert command[:4] == ["gait", "gate", "eval", "--policy"] + assert str(tmp_path / "policy.yaml") in command + assert "--intent" in command + assert "--delegation-token" in command + assert str(tmp_path / "delegation.json") in command + assert "--delegation-token-chain" in command + assert f"{tmp_path / 'delegation-a.json'},{tmp_path / 'delegation-b.json'}" in command + assert "--delegation-public-key" in command + assert str(tmp_path / "delegation.pub") in command + assert "--delegation-public-key-env" in command + assert "GAIT_DELEGATION_PUBLIC_KEY" in command + assert "--delegation-private-key" in command + assert str(tmp_path / "delegation.key") in command + assert "--delegation-private-key-env" in command + assert "GAIT_DELEGATION_PRIVATE_KEY" in command + + +def test_evaluate_gate_non_verdict_error_raises_command_error( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + intent = capture_intent( + tool_name="tool.allow", + args={"path": "/tmp/out.txt"}, + context=IntentContext(identity="alice", workspace="/repo/gait", risk_class="high"), + ) + + monkeypatch.setattr( + client_module, + "_run_command", + lambda command, cwd=None: client_module._CommandResult( + command=list(command), + exit_code=1, + stdout=json.dumps({"ok": False, "error": "gate exploded"}), + stderr="boom", + ), + ) + + with pytest.raises(client_module.GaitCommandError) as raised: + evaluate_gate(policy_path=tmp_path / "policy.yaml", intent=intent, gait_bin="gait") + assert "gate exploded" in str(raised.value) + + def test_internal_helpers_parse_json_and_prefix() -> None: assert client_module._parse_json_stdout("") is None assert client_module._parse_json_stdout("[]") is None @@ -404,3 +494,107 @@ def test_record_runpack_invalid_capture_mode_raises(tmp_path: Path) -> None: cwd=tmp_path, capture_mode="invalid", ) + + +def test_write_trace_rejects_missing_file_and_wrong_schema(tmp_path: Path) -> None: + with pytest.raises(client_module.GaitError) as missing: + write_trace(trace_path=tmp_path / "missing.json", destination_path=tmp_path / "out.json") + assert "trace file not found" in str(missing.value) + + wrong_schema = tmp_path / "wrong-trace.json" + wrong_schema.write_text( + json.dumps( + { + "schema_id": "gait.not_trace", + "schema_version": "1.0.0", + "created_at": "2026-02-05T00:00:00Z", + "producer_version": "0.0.0-dev", + "trace_id": "trace_1", + "tool_name": "tool.write", + "args_digest": "1" * 64, + "intent_digest": "2" * 64, + "policy_digest": "3" * 64, + "verdict": "allow", + } + ) + + "\n", + encoding="utf-8", + ) + + with pytest.raises(client_module.GaitError) as wrong: + write_trace(trace_path=wrong_schema, destination_path=tmp_path / "out.json") + assert "unexpected trace schema_id" in str(wrong.value) + + +def test_capture_demo_runpack_error_paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + client_module, + "_run_command", + lambda command, cwd=None: client_module._CommandResult( + command=list(command), + exit_code=1, + stdout=json.dumps({"ok": False, "error": "demo failed"}), + stderr="", + ), + ) + with pytest.raises(client_module.GaitCommandError) as raised: + capture_demo_runpack(gait_bin="gait", cwd=tmp_path) + assert "demo failed" in str(raised.value) + + monkeypatch.setattr( + client_module, + "_run_command", + lambda command, cwd=None: client_module._CommandResult( + command=list(command), + exit_code=0, + stdout=json.dumps({"ok": False}), + stderr="", + ), + ) + with pytest.raises(client_module.GaitError) as ok_false: + capture_demo_runpack(gait_bin="gait", cwd=tmp_path) + assert "ok=false" in str(ok_false.value) + + monkeypatch.setattr( + client_module, + "_run_command", + lambda command, cwd=None: client_module._CommandResult( + command=list(command), + exit_code=0, + stdout=json.dumps({"ok": True, "run_id": "", "bundle": ""}), + stderr="", + ), + ) + with pytest.raises(client_module.GaitError) as missing_fields: + capture_demo_runpack(gait_bin="gait", cwd=tmp_path) + assert "missing run_id or bundle" in str(missing_fields.value) + + +def test_create_regress_fixture_error_paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + client_module, + "_run_command", + lambda command, cwd=None: client_module._CommandResult( + command=list(command), + exit_code=1, + stdout=json.dumps({"ok": False, "error": "regress init failed"}), + stderr="", + ), + ) + with pytest.raises(client_module.GaitCommandError) as raised: + create_regress_fixture(from_run="run_demo", gait_bin="gait", cwd=tmp_path) + assert "regress init failed" in str(raised.value) + + monkeypatch.setattr( + client_module, + "_run_command", + lambda command, cwd=None: client_module._CommandResult( + command=list(command), + exit_code=0, + stdout=json.dumps({"ok": False}), + stderr="", + ), + ) + with pytest.raises(client_module.GaitError) as ok_false: + create_regress_fixture(from_run="run_demo", gait_bin="gait", cwd=tmp_path) + assert "ok=false" in str(ok_false.value)