-
Notifications
You must be signed in to change notification settings - Fork 837
feat(cli): add atomic launch receipts #895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
4f9ae3e
01165f7
eb7cd0e
4141f00
67bf0b2
c4d2789
a8d3b5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| --- | ||
| title: Launch Proof Fork Maintenance | ||
| description: Temporary release and retirement procedure for strict AXI launch receipts. | ||
| --- | ||
|
|
||
| Strict AXI launch receipts are upstream-first. Use a fork binary only while no | ||
| upstream release exposes the `axi run --launch-nonce` receipt contract. | ||
|
|
||
| ## Publish a temporary fork build | ||
|
|
||
| 1. Rebase the isolated proof commit series onto the current upstream `main`. | ||
| Do not carry unrelated custody, routing, or release changes. | ||
| 2. Build from an immutable fork commit and publish a public GitHub release whose | ||
| tag and release notes record that full commit SHA. | ||
| 3. Configure the consuming automation with that exact release asset URL and | ||
| commit SHA. Never point an updater or binary source at a mutable branch, | ||
| `latest`, or a moving release tag. | ||
| 4. Run the strict-mode smoke test against the installed fork binary: invoke | ||
| `no-mistakes axi run --intent <exact-intent> --launch-nonce <fresh-nonce>` | ||
| on a committed feature branch, and verify a pre-drive `launch_receipt` has | ||
| `created`, the full branch/head bindings, and the SHA-256 digest of the exact | ||
| persisted intent. Reinvoke the same request and verify the same run ID with | ||
| `reused`. | ||
|
|
||
| The nonce and intent digest are safe correlation material; do not add raw intent | ||
| to fork release notes, telemetry, status output, or update configuration. | ||
|
|
||
| ## Retire after upstream ships | ||
|
|
||
| Do not infer support from an upstream version number. Install the candidate | ||
| upstream release, inspect `no-mistakes axi run --help` for `--launch-nonce`, and | ||
| run the same smoke test above against the upstream binary. Only after that smoke | ||
| test passes: | ||
|
|
||
| 1. Remove the fork binary source/update override and switch consumers to the | ||
| verified upstream release. | ||
| 2. Delete the temporary fork release and proof branch. | ||
| 3. Remove this temporary maintenance path in the next upstream documentation | ||
| update; no compatibility alias or permanent fork-only command remains. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "crypto/sha256" | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
|
|
@@ -58,7 +59,7 @@ func newAxiRunCmd() *cobra.Command { | |
| var autoYes bool | ||
| var skipValue string | ||
| var intent string | ||
|
|
||
| var launchNonce string | ||
| cmd := &cobra.Command{ | ||
| Use: "run", | ||
| Short: "Validate your code changes, blocking until a decision point or the outcome", | ||
|
|
@@ -70,6 +71,10 @@ func newAxiRunCmd() *cobra.Command { | |
| "--intent is required when starting a new run: pass what the user set out\n" + | ||
| "to accomplish (the goal behind the change, not a description of the diff)\n" + | ||
| "so no-mistakes uses it directly instead of inferring it from transcripts.\n\n" + | ||
| "--launch-nonce enables strict proof mode. It must be a 1–128-character\n" + | ||
| "opaque URL-safe token. Before driving, AXI emits a receipt with the durable\n" + | ||
| "run ID, created/reused disposition, full heads, and a digest of the exact\n" + | ||
| "persisted intent; raw intent is never included.\n\n" + | ||
| "The calling agent drives AXI approval gates but does not become the pipeline\n" + | ||
| "agent. The daemon requires a supported native agent binary, the `agent: cursor`\n" + | ||
| "ACP alias, or an explicit `acp:<target>` through `acpx`, and fails before the\n" + | ||
|
|
@@ -80,26 +85,31 @@ func newAxiRunCmd() *cobra.Command { | |
| SilenceUsage: true, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| return trackAxiSurface("axi-run", "/axi/run", telemetry.Fields{ | ||
| "auto_yes": autoYes, | ||
| "has_intent": strings.TrimSpace(intent) != "", | ||
| "has_skip": strings.TrimSpace(skipValue) != "", | ||
| "auto_yes": autoYes, | ||
| "has_intent": strings.TrimSpace(intent) != "", | ||
| "has_skip": strings.TrimSpace(skipValue) != "", | ||
| "has_launch_nonce": launchNonce != "", | ||
| }, func() error { | ||
| skipSteps, err := parseSkipSteps(skipValue) | ||
| if err != nil { | ||
| return emitError(cmd, 2, err.Error(), | ||
| "Valid steps: intent, rebase, review, test, document, lint, push, pr, ci") | ||
| } | ||
| return runAxiRun(cmd, autoYes, skipSteps, intent) | ||
| return runAxiRunWithLaunchNonce(cmd, autoYes, skipSteps, intent, launchNonce) | ||
| }) | ||
| }, | ||
| } | ||
| cmd.Flags().BoolVarP(&autoYes, "yes", "y", false, "auto-resolve every gate (fix findings, then accept) until a decision point or outcome") | ||
| cmd.Flags().StringVar(&skipValue, "skip", "", "comma-separated pipeline steps to skip") | ||
| cmd.Flags().StringVar(&intent, "intent", "", "what the user set out to accomplish (not a description of the diff); used instead of inferring from transcripts (required to start a run)") | ||
| return cmd | ||
| cmd.Flags().StringVar(&launchNonce, "launch-nonce", "", "opaque nonce for a daemon-bound pre-drive launch receipt; enables strict proof mode") | ||
| } | ||
|
|
||
| func runAxiRun(cmd *cobra.Command, autoYes bool, skipSteps []types.StepName, intent string) error { | ||
| return runAxiRunWithLaunchNonce(cmd, autoYes, skipSteps, intent, "") | ||
| } | ||
|
|
||
| func runAxiRunWithLaunchNonce(cmd *cobra.Command, autoYes bool, skipSteps []types.StepName, intent, launchNonce string) error { | ||
| ctx := cmd.Context() | ||
| env, err := openAxiRunEnv() | ||
| if err != nil { | ||
|
|
@@ -121,7 +131,24 @@ func runAxiRun(cmd *cobra.Command, autoYes bool, skipSteps []types.StepName, int | |
| return emitError(cmd, 1, fmt.Sprintf("get current HEAD: %v", err)) | ||
| } | ||
|
|
||
| runID := activeRunID(env, branch, headSHA) | ||
| runID := "" | ||
| var launchReceipt *ipc.LaunchReceipt | ||
| if launchNonce != "" { | ||
| receipt, err := lookupLaunchReceipt(env.client, env.repo.ID, branch, launchNonce) | ||
| if err != nil { | ||
| return emitError(cmd, 1, fmt.Sprintf("look up launch receipt: %v", err)) | ||
| } | ||
| if receipt != nil { | ||
| if receipt.HeadSHA != headSHA || receipt.SubmittedHeadSHA != headSHA || receipt.IntentDigest != digestLaunchIntent(intent) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a nonce is already bound and another push has since advanced the gate branch, a caller whose local checkout remains at the old head takes this lookup shortcut, emits the old receipt, and drives its run without invoking Useful? React with 👍 / 👎. |
||
| return emitError(cmd, 1, "conflicting launch receipt: nonce is already bound to a different head or intent", | ||
| "Use a new --launch-nonce for a changed request") | ||
| } | ||
| launchReceipt = receipt | ||
| runID = receipt.RunID | ||
| } | ||
| } else { | ||
| runID = activeRunID(env, branch, headSHA) | ||
| } | ||
| if runID == "" { | ||
| if err := configErrorForFreshAxiRun(env, runID); err != nil { | ||
| return emitError(cmd, 1, err.Error(), repoInitHelp(err)...) | ||
|
|
@@ -142,14 +169,24 @@ func runAxiRun(cmd *cobra.Command, autoYes bool, skipSteps []types.StepName, int | |
| return guard(cmd) | ||
| } | ||
| var err error | ||
| runID, err = triggerRun(ctx, env, branch, headSHA, skipSteps, intent) | ||
| if launchNonce != "" { | ||
| launchReceipt, err = triggerProofRun(ctx, env, branch, headSHA, skipSteps, intent, launchNonce) | ||
| if err == nil { | ||
| runID = launchReceipt.RunID | ||
| } | ||
| } else { | ||
| runID, err = triggerRun(ctx, env, branch, headSHA, skipSteps, intent) | ||
| } | ||
| if err != nil { | ||
| if ownershipErr, ok := err.(*branchOwnershipError); ok { | ||
| return emitBranchOwnershipError(cmd, ownershipErr) | ||
| } | ||
| return emitError(cmd, 1, err.Error()) | ||
| } | ||
| } | ||
| if launchReceipt != nil { | ||
| emitLaunchReceipt(cmd, *launchReceipt) | ||
| } | ||
|
|
||
| run, ciReady, err := driveRun(ctx, cmd.ErrOrStderr(), env.client, env.p.Socket(), runID, autoYes) | ||
| if err != nil { | ||
|
|
@@ -158,6 +195,11 @@ func runAxiRun(cmd *cobra.Command, autoYes bool, skipSteps []types.StepName, int | |
| return renderDriveResult(cmd, run, ciReady) | ||
| } | ||
|
|
||
| func digestLaunchIntent(intent string) string { | ||
| sum := sha256.Sum256([]byte(intent)) | ||
| return fmt.Sprintf("%x", sum) | ||
| } | ||
|
|
||
| func configErrorForFreshAxiRun(env *axiEnv, runID string) error { | ||
| if runID != "" { | ||
| return nil | ||
|
|
@@ -354,6 +396,79 @@ func runsForHead(client *ipc.Client, repoID, branch, headSHA string) ([]ipc.RunI | |
| return result.Runs, nil | ||
| } | ||
|
|
||
| func lookupLaunchReceipt(client *ipc.Client, repoID, branch, launchNonce string) (*ipc.LaunchReceipt, error) { | ||
| var result ipc.GetLaunchReceiptResult | ||
| if err := client.Call(ipc.MethodGetLaunchReceipt, &ipc.GetLaunchReceiptParams{ | ||
| RepoID: repoID, Branch: branch, LaunchNonce: launchNonce, | ||
| }, &result); err != nil { | ||
| return nil, err | ||
| } | ||
| return result.Receipt, nil | ||
| } | ||
|
|
||
| // triggerProofRun captures the immutable commit selected before the push and | ||
| // waits only for the daemon's nonce-bound receipt. A same-head run selected by | ||
| // ordinary active-run heuristics is never accepted as proof. | ||
|
|
||
| func claimLaunchReceipt(client *ipc.Client, repoID, branch, launchNonce string) (*ipc.LaunchReceipt, error) { | ||
| var result ipc.GetLaunchReceiptResult | ||
| if err := client.Call(ipc.MethodClaimLaunchReceipt, &ipc.GetLaunchReceiptParams{ | ||
| RepoID: repoID, Branch: branch, LaunchNonce: launchNonce, | ||
| }, &result); err != nil { | ||
| return nil, err | ||
| } | ||
| return result.Receipt, nil | ||
| } | ||
| func triggerProofRun(ctx context.Context, env *axiEnv, branch, headSHA string, skipSteps []types.StepName, intent, launchNonce string) (*ipc.LaunchReceipt, error) { | ||
| pushOptions := formatSkipPushOptions(skipSteps) | ||
| pushOptions = append(pushOptions, formatIntentPushOption(intent), formatLaunchNoncePushOption(launchNonce)) | ||
| if state := freshRunBranchOwnershipState(ctx, env); state != nil { | ||
| return nil, &branchOwnershipError{state: *state} | ||
| } | ||
| pushErr := git.PushCommitWithOptions(ctx, ".", gate.RemoteName, headSHA, "refs/heads/"+branch, "", false, pushOptions) | ||
| if pushErr != nil { | ||
| if state := freshRunBranchOwnershipState(ctx, env); state != nil { | ||
| return nil, &branchOwnershipError{state: *state} | ||
| } | ||
| return nil, fmt.Errorf("push %q to gate: %w", branch, pushErr) | ||
| } | ||
| if receipt, err := waitForLaunchReceipt(ctx, env.client, env.repo.ID, branch, launchNonce, triggerWaitTimeout); err != nil { | ||
| return nil, err | ||
| } else if receipt != nil { | ||
| return receipt, nil | ||
| } | ||
| var result ipc.StartFreshRunResult | ||
| if err := env.client.Call(ipc.MethodStartFreshRun, &ipc.StartFreshRunParams{ | ||
| RepoID: env.repo.ID, Branch: branch, HeadSHA: headSHA, SkipSteps: skipSteps, Intent: intent, LaunchNonce: launchNonce, | ||
| }, &result); err != nil { | ||
| return nil, fmt.Errorf("start fresh run: %w", err) | ||
| } | ||
| return &result.Receipt, nil | ||
| } | ||
|
|
||
| func waitForLaunchReceipt(ctx context.Context, client *ipc.Client, repoID, branch, launchNonce string, timeout time.Duration) (*ipc.LaunchReceipt, error) { | ||
| deadline := time.NewTimer(timeout) | ||
| defer deadline.Stop() | ||
| poll := time.NewTicker(150 * time.Millisecond) | ||
| defer poll.Stop() | ||
| for { | ||
| receipt, err := lookupLaunchReceipt(client, repoID, branch, launchNonce) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if receipt != nil { | ||
| return claimLaunchReceipt(client, repoID, branch, launchNonce) | ||
| } | ||
| select { | ||
| case <-ctx.Done(): | ||
| return nil, ctx.Err() | ||
| case <-deadline.C: | ||
| return nil, nil | ||
| case <-poll.C: | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // waitForTriggeredRunForHead waits for the run created by this trigger. The | ||
| // active-run lookup handles normal execution; the head lookup catches a run | ||
| // that fails before it can be observed as active. priorRunIDs prevents an | ||
|
|
@@ -576,6 +691,21 @@ func sendRespond(client *ipc.Client, runID string, step types.StepName, action t | |
| return nil | ||
| } | ||
|
|
||
| // emitLaunchReceipt writes the proof document before driveRun subscribes, so a | ||
| // caller retains the daemon-authored binding even when driving later blocks or | ||
| // returns at a gate. It intentionally contains only an opaque nonce and digest. | ||
| func emitLaunchReceipt(cmd *cobra.Command, receipt ipc.LaunchReceipt) { | ||
| emitDoc(cmd, toon.Field{Key: "launch_receipt", Value: toon.NewObject( | ||
| toon.Field{Key: "run_id", Value: receipt.RunID}, | ||
| toon.Field{Key: "disposition", Value: receipt.Disposition}, | ||
| toon.Field{Key: "launch_nonce", Value: receipt.LaunchNonce}, | ||
| toon.Field{Key: "branch", Value: receipt.Branch}, | ||
| toon.Field{Key: "head_sha", Value: receipt.HeadSHA}, | ||
| toon.Field{Key: "submitted_head_sha", Value: receipt.SubmittedHeadSHA}, | ||
| toon.Field{Key: "intent_digest", Value: receipt.IntentDigest}, | ||
| )}) | ||
| } | ||
|
|
||
| // renderDriveResult prints the run snapshot plus one of: the active gate (exit | ||
| // 0, a normal decision point), a checks-passed outcome (exit 0, CI readiness is | ||
| // established by green checks or the trusted no_ci declaration and the PR is | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.