Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,21 @@ jobs:
run: |
make test-v1-8-acceptance

script-intent-acceptance:
needs: changes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Script intent governance acceptance checks
run: |
make test-script-intent-acceptance

release-smoke:
needs: changes
runs-on: ubuntu-latest
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ BENCH_REGEX := Benchmark(EvaluatePolicyTypical|VerifyZipTypical|DiffRunpacksTypi
BENCH_OUTPUT ?= perf/bench_output.txt
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-ui-acceptance test-ui-unit test-ui-e2e-smoke test-ui-perf 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 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: 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-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 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

Expand Down Expand Up @@ -155,6 +155,10 @@ test-packspec-tck:
$(GO) build -o ./gait ./cmd/gait
bash scripts/test_packspec_tck.sh ./gait

test-script-intent-acceptance:
$(GO) build -o ./gait ./cmd/gait
bash scripts/test_script_intent_acceptance.sh ./gait

test-ui-acceptance:
$(GO) build -o ./gait ./cmd/gait
bash scripts/test_ui_acceptance.sh ./gait
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ See: [2,880 tool calls gate-checked in 24 hours](docs/blog/openclaw_24h_boundary

**Signed packs** — every run and job emits a tamper-evident artifact (Ed25519 + SHA-256 manifest). Verify offline. Attach to PRs, incidents, audits. One artifact is the entire proof. Export OTEL-style JSONL and deterministic PostgreSQL index SQL with `gait pack export`.

**Fail-closed policy enforcement** — `gait gate eval` evaluates a structured tool-call intent against YAML policy before the side effect runs. Non-allow means non-execute. Signed trace proves the decision.
**Fail-closed policy enforcement** — `gait gate eval` evaluates a structured tool-call intent against YAML policy before the side effect runs. Non-allow means non-execute. Signed trace proves the decision. Script mode supports deterministic step rollups, optional Wrkr context enrichment, and signed approved-script fast-path allow.

**Incident → CI gate in one command** — `gait regress bootstrap` converts a bad run into a permanent regression fixture with JUnit output. Exit 0 = pass, exit 5 = drift. Never debug the same failure twice.

Expand Down Expand Up @@ -202,7 +202,9 @@ gait job approve|cancel|inspect Job approval and inspection
gait pack build|verify|inspect|diff|export Unified pack operations + OTEL/Postgres sinks
gait regress init|bootstrap|run Incident → CI gate
gait gate eval Policy enforcement + signed trace
gait approve-script Mint signed approved-script registry entries
gait approve Mint signed approval tokens
gait list-scripts Inspect approved-script registry status
gait delegate mint|verify Delegation token lifecycle
gait report top Rank highest-risk actions
gait voice token mint|verify Voice commitment gating
Expand Down
186 changes: 186 additions & 0 deletions cmd/gait/approve_script.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package main

import (
"flag"
"fmt"
"io"
"strings"
"time"

"github.com/Clyra-AI/gait/core/gate"
schemagate "github.com/Clyra-AI/gait/core/schema/v1/gate"
sign "github.com/Clyra-AI/proof/signing"
)

type approveScriptOutput struct {
OK bool `json:"ok"`
PatternID string `json:"pattern_id,omitempty"`
PolicyDigest string `json:"policy_digest,omitempty"`
ScriptHash string `json:"script_hash,omitempty"`
ToolSequence []string `json:"tool_sequence,omitempty"`
RegistryPath string `json:"registry_path,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
Error string `json:"error,omitempty"`
}

func runApproveScript(arguments []string) int {
if hasExplainFlag(arguments) {
return writeExplain("Create a signed approved-script registry entry bound to a policy digest and script hash.")
}
flagSet := flag.NewFlagSet("approve-script", flag.ContinueOnError)
flagSet.SetOutput(io.Discard)

var policyPath string
var intentPath string
var registryPath string
var patternID string
var approver string
var ttlRaw string
var scopeCSV string
var keyMode string
var privateKeyPath string
var privateKeyEnv string
var jsonOutput bool
var helpFlag bool

flagSet.StringVar(&policyPath, "policy", "", "path to policy yaml")
flagSet.StringVar(&intentPath, "intent", "", "path to script intent json")
flagSet.StringVar(&registryPath, "registry", "", "path to approved script registry json")
flagSet.StringVar(&patternID, "pattern-id", "", "approved script pattern id")
flagSet.StringVar(&approver, "approver", "", "approver identity")
flagSet.StringVar(&ttlRaw, "ttl", "168h", "entry validity duration")
flagSet.StringVar(&scopeCSV, "scope", "", "comma-separated scope values")
flagSet.StringVar(&keyMode, "key-mode", "dev", "signing key mode: dev or prod")
flagSet.StringVar(&privateKeyPath, "private-key", "", "path to base64 private signing key")
flagSet.StringVar(&privateKeyEnv, "private-key-env", "", "env var containing base64 private signing key")
flagSet.BoolVar(&jsonOutput, "json", false, "emit JSON output")
flagSet.BoolVar(&helpFlag, "help", false, "show help")

if err := flagSet.Parse(arguments); err != nil {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}
if helpFlag {
printApproveScriptUsage()
return exitOK
}
if len(flagSet.Args()) > 0 {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: "unexpected positional arguments"}, exitInvalidInput)
}
if strings.TrimSpace(policyPath) == "" || strings.TrimSpace(intentPath) == "" || strings.TrimSpace(registryPath) == "" || strings.TrimSpace(approver) == "" {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{
OK: false,
Error: "--policy, --intent, --registry, and --approver are required",
}, exitInvalidInput)
}

ttl, err := time.ParseDuration(strings.TrimSpace(ttlRaw))
if err != nil || ttl <= 0 {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: "invalid --ttl duration"}, exitInvalidInput)
}
policy, err := gate.LoadPolicyFile(policyPath)
if err != nil {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}
policyDigest, err := gate.PolicyDigest(policy)
if err != nil {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}
intent, err := readIntentRequest(intentPath)
if err != nil {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}
normalizedIntent, err := gate.NormalizeIntent(intent)
if err != nil {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}
if normalizedIntent.Script == nil || len(normalizedIntent.Script.Steps) == 0 {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: "intent must include script.steps"}, exitInvalidInput)
}
scriptHash, err := gate.ScriptHash(normalizedIntent)
if err != nil {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}
toolSequence := make([]string, 0, len(normalizedIntent.Script.Steps))
for _, step := range normalizedIntent.Script.Steps {
toolSequence = append(toolSequence, step.ToolName)
}

if strings.TrimSpace(patternID) == "" {
patternID = "pattern_" + scriptHash[:12]
}
nowUTC := time.Now().UTC()
keyPair, _, err := sign.LoadSigningKey(sign.KeyConfig{
Mode: sign.KeyMode(strings.ToLower(strings.TrimSpace(keyMode))),
PrivateKeyPath: privateKeyPath,
PrivateKeyEnv: privateKeyEnv,
})
if err != nil {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}

entry, err := gate.SignApprovedScriptEntry(schemagate.ApprovedScriptEntry{
SchemaID: "gait.gate.approved_script_entry",
SchemaVersion: "1.0.0",
CreatedAt: nowUTC,
ProducerVersion: version,
PatternID: strings.TrimSpace(patternID),
PolicyDigest: policyDigest,
ScriptHash: scriptHash,
ToolSequence: toolSequence,
Scope: parseCSV(scopeCSV),
ApproverIdentity: strings.TrimSpace(approver),
ExpiresAt: nowUTC.Add(ttl),
}, keyPair.Private)
if err != nil {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}

existing, err := gate.ReadApprovedScriptRegistry(registryPath)
if err != nil {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}
updated := make([]schemagate.ApprovedScriptEntry, 0, len(existing)+1)
replaced := false
for _, candidate := range existing {
if candidate.PatternID == entry.PatternID {
updated = append(updated, entry)
replaced = true
continue
}
updated = append(updated, candidate)
}
if !replaced {
updated = append(updated, entry)
}
if err := gate.WriteApprovedScriptRegistry(registryPath, updated); err != nil {
return writeApproveScriptOutput(jsonOutput, approveScriptOutput{OK: false, Error: err.Error()}, exitCodeForError(err, exitInvalidInput))
}

return writeApproveScriptOutput(jsonOutput, approveScriptOutput{
OK: true,
PatternID: entry.PatternID,
PolicyDigest: entry.PolicyDigest,
ScriptHash: entry.ScriptHash,
ToolSequence: entry.ToolSequence,
RegistryPath: strings.TrimSpace(registryPath),
ExpiresAt: entry.ExpiresAt.UTC().Format(time.RFC3339Nano),
}, exitOK)
}

func writeApproveScriptOutput(jsonOutput bool, output approveScriptOutput, exitCode int) int {
if jsonOutput {
return writeJSONOutput(output, exitCode)
}
if !output.OK {
fmt.Printf("approve-script error: %s\n", output.Error)
return exitCode
}
fmt.Printf("approve-script: pattern=%s registry=%s\n", output.PatternID, output.RegistryPath)
fmt.Printf("policy_digest=%s script_hash=%s\n", output.PolicyDigest, output.ScriptHash)
return exitCode
}

func printApproveScriptUsage() {
fmt.Println("Usage:")
fmt.Println(" gait approve-script --policy <policy.yaml> --intent <script_intent.json> --registry <registry.json> --approver <identity> [--pattern-id <id>] [--ttl 168h] [--scope <csv>] [--key-mode dev|prod] [--private-key <path>|--private-key-env <VAR>] [--json] [--explain]")
}
Loading
Loading