@axtary/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
+ import { readFileSync } from "node:fs";
3
4
  import { chmod, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
5
  import { dirname, extname, relative, resolve } from "node:path";
5
6
  import { createInterface } from "node:readline";
@@ -8,10 +9,11 @@ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
8
9
  import { ACTIONPASS_V2_VERSION, ACTIONPASS_V1_VERSION, analyzePostgresSelect, AxtaryDecisionSchema, FileDpopReplayStore, LocalActionPassStatusStore, LocalCaepMappingStore, LocalSsfReplayStore, LocalActionPassTrustStore, createApprovalArtifact, demoAction, hashPayload, issuerJwks, issuerSigningKey, issuerVerificationKeyResolver, explainDecision, loadOrCreateIssuerKeyring, parseNormalizedAction, processCaepSessionRevoked, rotateIssuerKeyring, signStatusListToken, } from "@axtary/actionpass";
9
10
  import { createFakeAdapterState, createFakeHandlers, buildNativeConnectorDoctorPlans, createAwsRestHandlers, createActionPassVerifiedHandler, createGcpRestHandlers, createGitHubRestHandlers, createJiraRestHandlers, createLinearGraphqlHandlers, createLocalDocsHandlers, createPostgresNativeHandlers, createDriveRestHandlers, createSlackWebHandlers, GITHUB_NATIVE_CONNECTOR, githubAppRequiredEnv, mintGithubInstallationToken, resolveGithubAppCredentials, smokeAwsRestCredentials, smokeGcpRestCredentials, smokeGithubAppCredentials, smokeGitHubRestCredentials, smokeJiraRestCredentials, smokeLinearGraphqlCredentials, smokeLocalDocsRoots, smokePostgresCredentials, smokeDriveRestCredentials, smokeSlackWebCredentials, } from "@axtary/adapters";
10
11
  import { loadAxtaryConfig, CachedConfigLoader } from "@axtary/config";
11
- import { analyzeForensics, attestLedgerExport, buildLedgerAttestationBundle, exportLedgerRecords, formatLedgerExport, LedgerAttestationBundleSchema, LedgerExportFormatSchema, LocalJsonlLedger, OtlpHttpTraceExporter, proveLedgerConsistency, proveLedgerInclusion, syncLedgerExport, exportLedgerRecordsToOtlp, verifyLedgerAttestationBundle, verifyLedgerConsistencyProof, verifyLedgerInclusionProof, } from "@axtary/ledger";
12
+ import { analyzeForensics, attestLedgerExport, buildLedgerAttestationBundle, CROSS_ISSUER_TRUST_ROOT_VERSION, evaluateLedgerEquivalence, exportLedgerRecords, formatLedgerExport, LedgerAttestationBundleSchema, LedgerExportFormatSchema, LocalJsonlLedger, OtlpHttpTraceExporter, proveLedgerConsistency, proveLedgerInclusion, syncLedgerExport, exportLedgerRecordsToOtlp, verifyLedgerAttestationBundle, verifyLedgerConsistencyProof, verifyLedgerInclusionProof, } from "@axtary/ledger";
12
13
  import { MCP_TOOL_CALL, McpHttpClient, McpStdioClient, createMcpAction, createMcpFixtureToolHandler, createMcpToolDefinition, createMcpToolRouter, createMcpWrapperServer, createMcpWrapperTool, evaluateMcpVersionChain, findMcpSignedDefinition, findMcpToolPin, loadMcpConformanceRegistry, loadMcpPinManifest, loadMcpPublisherTrustStore, loadMcpSignedDefinitionRegistry, McpPublisherKeySchema, mcpPublisherKeyResolver, pinMcpDefinitions, reconcileMcpPins, saveMcpConformanceRegistry, saveMcpPinManifest, saveMcpPublisherTrustStore, trustMcpPublisher, upsertMcpConformanceReceipt, verifyMcpSignedDefinition, } from "@axtary/mcp";
13
14
  import { createPollingHostedApprovalResolver } from "@axtary/approvals";
14
15
  import { applyPolicyTemplate, evaluatePolicy, explainPolicyDecision, getPolicyTemplate, getPolicyTemplates, runPolicyFixtures as runPolicyTemplateFixtures, simulatePolicy, } from "@axtary/policy";
16
+ import { actionFromAcsToolCallSnapshot, createAxtaryAcsManifest, evaluateAcsIntervention, } from "@axtary/policy/acs";
15
17
  import { createProxyRuntime, } from "@axtary/proxy";
16
18
  import { runClaudeCodeHook } from "./claude-code-hook.js";
17
19
  import { runCodexHook } from "./codex-hook.js";
@@ -37,6 +39,7 @@ export const LEDGER_OTEL_EXPORT_RUN_SCHEMA_VERSION = "axtary.ledger_otel_export_
37
39
  export const LEDGER_ATTEST_RUN_SCHEMA_VERSION = "axtary.ledger_attest_run.v0";
38
40
  export const LEDGER_VERIFY_EXPORT_RUN_SCHEMA_VERSION = "axtary.ledger_verify_export_run.v0";
39
41
  export const LEDGER_PROVE_INCLUSION_RUN_SCHEMA_VERSION = "axtary.ledger_prove_inclusion_run.v0";
42
+ export const LEDGER_PROVE_EQUIVALENCE_RUN_SCHEMA_VERSION = "axtary.ledger_prove_equivalence_run.v0";
40
43
  export const LEDGER_PROVE_CONSISTENCY_RUN_SCHEMA_VERSION = "axtary.ledger_prove_consistency_run.v0";
41
44
  export const LEDGER_VERIFY_PROOF_RUN_SCHEMA_VERSION = "axtary.ledger_verify_proof_run.v0";
42
45
  export const LEDGER_FORENSICS_RUN_SCHEMA_VERSION = "axtary.ledger_forensics_run.v0";
@@ -95,6 +98,7 @@ export function authorizeDecisionHints(decision, policy) {
95
98
  return hints;
96
99
  }
97
100
  export const POLICY_PARITY_SCHEMA_VERSION = "axtary.policy_parity.v0";
101
+ export const ACS_CONFORMANCE_SCHEMA_VERSION = "axtary.acs_conformance.v0";
98
102
  const MCP_DEMO_TOOL_DEFINITION = createMcpToolDefinition({
99
103
  serverIdentity: "mcp:axtary-demo-fixtures",
100
104
  serverVersion: "1.0.0",
@@ -295,6 +299,9 @@ export async function startProxyServer(input = {}) {
295
299
  endpoint: input.otlpEndpoint,
296
300
  serviceName: input.otlpServiceName ?? "axtary-local-proxy",
297
301
  timeoutMs: input.otlpTimeoutMs,
302
+ // The ledger library no longer falls back to ambient fetch; the
303
+ // CLI is the app boundary that supplies the transport.
304
+ fetch: globalThis.fetch,
298
305
  }),
299
306
  }
300
307
  : {});
@@ -375,6 +382,20 @@ export async function startProxyServer(input = {}) {
375
382
  writeJson(response, 200, issuerJwks(keyring));
376
383
  return;
377
384
  }
385
+ if (request.method === "GET" &&
386
+ request.url === "/.well-known/axtary-trust-root.json") {
387
+ writeJson(response, 200, {
388
+ schemaVersion: CROSS_ISSUER_TRUST_ROOT_VERSION,
389
+ issuer: loadedConfig.config.issuer,
390
+ jwks: issuerJwks(keyring),
391
+ statusListUri,
392
+ federationProfile: {
393
+ profile: "axtary.openid-federation-anchor.v0",
394
+ entityStatementTyp: "entity-statement+jwt",
395
+ },
396
+ });
397
+ return;
398
+ }
378
399
  if (request.method === "GET" &&
379
400
  request.url === "/statuslists/actionpasses") {
380
401
  const signed = await signStatusListToken({
@@ -421,6 +442,7 @@ export async function startProxyServer(input = {}) {
421
442
  issuer: caepConfig.issuer,
422
443
  audience: caepConfig.audience,
423
444
  jwksUri: caepConfig.jwksUri,
445
+ fetch: globalThis.fetch,
424
446
  mappingStore: caepMappingStore,
425
447
  replayStore: caepReplayStore,
426
448
  revoke: async (rootPassId, claims) => {
@@ -564,7 +586,7 @@ runtime:
564
586
  mappingPath: .axtary/caep-mappings.json
565
587
  replayPath: .axtary/ssf-replay.json
566
588
  adapters:
567
- # Start in fake mode: deterministic local adapters, no credentials needed.
589
+ # Start in credential-free deterministic demo mode; no provider credentials needed.
568
590
  # Switch a provider to its real mode (rest/web/graphql) once its env token is set.
569
591
  github:
570
592
  mode: fake
@@ -686,7 +708,7 @@ export function formatInitResult(result) {
686
708
  });
687
709
  const rail = [
688
710
  " Next steps (the ten-minute rail — https://docs.axtary.com/quickstart):",
689
- " 1. axtary doctor connectors --config axtary.yml # readiness checklist; fake mode needs no credentials",
711
+ " 1. axtary doctor connectors --config axtary.yml # readiness checklist; demo mode needs no credentials",
690
712
  ...policyTestStep.map((line) => ` ${line}`),
691
713
  ` ${gateStepNumber}. Gate a live agent — https://docs.axtary.com/integrate-agent (Claude Code / Cursor / Codex / MCP)`,
692
714
  "",
@@ -1000,7 +1022,11 @@ async function connectMcpUpstream(target) {
1000
1022
  });
1001
1023
  headers = { ...headers, authorization };
1002
1024
  }
1003
- client = await McpHttpClient.start({ url: target.wrapUrl, headers });
1025
+ client = await McpHttpClient.start({
1026
+ url: target.wrapUrl,
1027
+ headers,
1028
+ fetch: globalThis.fetch,
1029
+ });
1004
1030
  }
1005
1031
  else if (target.wrap) {
1006
1032
  const [command, ...commandArgs] = target.wrap.split(/\s+/).filter(Boolean);
@@ -1899,6 +1925,17 @@ export async function runDemo(input = {}) {
1899
1925
  verificationKey: publicKey,
1900
1926
  }),
1901
1927
  },
1928
+ // Same exact-payload approval primitive as the real workflows: the
1929
+ // artifact binds the payload hash, and the proxy stamps the
1930
+ // approved/executed hash pair on the execution record.
1931
+ approvalResolver: input.approveStepUp
1932
+ ? async ({ action }) => createApprovalArtifact({
1933
+ action,
1934
+ mode: "human",
1935
+ approvedBy: input.approvedBy ?? "user:local-reviewer",
1936
+ reason: "Local exact approval for the demo step-up action.",
1937
+ }).artifact
1938
+ : undefined,
1902
1939
  });
1903
1940
  const results = [];
1904
1941
  for (const demo of DEMO_ACTIONS) {
@@ -2591,18 +2628,172 @@ export async function runPolicyParity(input = {}) {
2591
2628
  results.push({ id: fixture.id, passed, expected, engines });
2592
2629
  }
2593
2630
  const failed = results.filter((result) => !result.passed).length;
2631
+ const agentCore = parity.buildAgentCoreCedarExport({
2632
+ fixtures: fixturesFile.fixtures.map((fixture) => ({
2633
+ id: fixture.id,
2634
+ action: fixture.action,
2635
+ })),
2636
+ policyVersion: policyConfig.version,
2637
+ });
2594
2638
  return {
2595
2639
  schemaVersion: POLICY_PARITY_SCHEMA_VERSION,
2596
2640
  engines: ["native", "cedar", "rego"],
2597
2641
  fixturesPath,
2598
2642
  cedarPath,
2599
2643
  regoWasmPath,
2644
+ agentCore: {
2645
+ schemaVersion: agentCore.schemaVersion,
2646
+ status: agentCore.status,
2647
+ gatewayResource: agentCore.gatewayResource,
2648
+ fixtureCount: agentCore.fixtures.length,
2649
+ sample: (agentCore.fixtures[0] ?? null),
2650
+ },
2600
2651
  passed: failed === 0,
2601
2652
  total: results.length,
2602
2653
  failed,
2603
2654
  results,
2604
2655
  };
2605
2656
  }
2657
+ export async function runAcsConformance(input = {}) {
2658
+ const loadedConfig = await loadAxtaryConfig({
2659
+ filePath: input.configPath,
2660
+ cwd: input.cwd,
2661
+ });
2662
+ const cwd = resolve(input.cwd ?? process.cwd());
2663
+ const ledgerPath = input.ledgerPath
2664
+ ? resolve(cwd, input.ledgerPath)
2665
+ : resolve(cwd, ".axtary/acs-conformance.jsonl");
2666
+ const ledger = new LocalJsonlLedger(ledgerPath);
2667
+ const { privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
2668
+ const manifest = createAxtaryAcsManifest();
2669
+ const runtime = createProxyRuntime({
2670
+ issuer: loadedConfig.config.issuer,
2671
+ tenant: loadedConfig.config.tenant,
2672
+ ledger,
2673
+ policy: loadedConfig.config.policy,
2674
+ signingKey: privateKey,
2675
+ keyId: "local-acs-conformance-key",
2676
+ allowUnsignedExecution: false,
2677
+ handlerTimeoutMs: loadedConfig.config.runtime.handlerTimeoutMs,
2678
+ budget: loadedConfig.config.runtime.budget,
2679
+ handlers: {
2680
+ "slack.chat.postMessage": async () => ({
2681
+ ok: true,
2682
+ provider: "acs-fixture",
2683
+ }),
2684
+ },
2685
+ });
2686
+ const vectors = [
2687
+ {
2688
+ id: "pre-tool-call-allow",
2689
+ interventionPoint: "pre_tool_call",
2690
+ snapshot: acsSlackSnapshot({
2691
+ channel: "#axtary-dev",
2692
+ message: "M9.4 ACS conformance allow",
2693
+ }),
2694
+ expectedVerdict: "allow",
2695
+ expectedProxyStatus: "executed",
2696
+ mode: "handle",
2697
+ },
2698
+ {
2699
+ id: "pre-tool-call-deny",
2700
+ interventionPoint: "pre_tool_call",
2701
+ snapshot: acsSlackSnapshot({
2702
+ channel: "#not-allowed",
2703
+ message: "M9.4 ACS conformance deny",
2704
+ }),
2705
+ expectedVerdict: "deny",
2706
+ expectedProxyStatus: "blocked",
2707
+ mode: "handle",
2708
+ },
2709
+ {
2710
+ id: "pre-tool-call-step-up",
2711
+ interventionPoint: "pre_tool_call",
2712
+ snapshot: acsSlackSnapshot({
2713
+ channel: "#axtary-dev",
2714
+ message: "M9.4 ACS conformance step-up",
2715
+ externalRecipients: ["customer@example.com"],
2716
+ }),
2717
+ expectedVerdict: "escalate",
2718
+ expectedProxyStatus: "step_up",
2719
+ mode: "authorize",
2720
+ },
2721
+ ];
2722
+ const results = [];
2723
+ for (const vector of vectors) {
2724
+ const acs = evaluateAcsIntervention({
2725
+ manifest,
2726
+ interventionPoint: vector.interventionPoint,
2727
+ snapshot: vector.snapshot,
2728
+ config: loadedConfig.config.policy,
2729
+ });
2730
+ const action = actionFromAcsToolCallSnapshot(vector.snapshot);
2731
+ const proxy = vector.mode === "authorize"
2732
+ ? await runtime.authorize(action)
2733
+ : await runtime.handle(action);
2734
+ const proxyStatus = "status" in proxy ? proxy.status : null;
2735
+ const ledgerLine = "ledger" in proxy ? proxy.ledger.lineNumber : null;
2736
+ const executionLedgerLine = "executionLedger" in proxy ? proxy.executionLedger.lineNumber : null;
2737
+ const actionPassId = "actionPass" in proxy && proxy.actionPass
2738
+ ? proxy.actionPass.claims.jti
2739
+ : null;
2740
+ const passed = acs.verdict === vector.expectedVerdict &&
2741
+ proxyStatus === vector.expectedProxyStatus &&
2742
+ (vector.expectedProxyStatus !== "executed" || actionPassId !== null) &&
2743
+ ledgerLine !== null;
2744
+ results.push({
2745
+ id: vector.id,
2746
+ interventionPoint: vector.interventionPoint,
2747
+ expectedVerdict: vector.expectedVerdict,
2748
+ actualVerdict: acs.verdict,
2749
+ expectedProxyStatus: vector.expectedProxyStatus,
2750
+ actualProxyStatus: proxyStatus,
2751
+ passed,
2752
+ reasons: acs.reasons,
2753
+ actionHash: acs.evidence.actionHash ?? null,
2754
+ payloadHash: acs.evidence.payloadHash ?? null,
2755
+ actionPassId,
2756
+ ledgerLine,
2757
+ executionLedgerLine,
2758
+ });
2759
+ }
2760
+ const failed = results.filter((result) => !result.passed).length;
2761
+ return {
2762
+ schemaVersion: ACS_CONFORMANCE_SCHEMA_VERSION,
2763
+ manifestVersion: manifest.agent_control_specification_version,
2764
+ ledgerPath,
2765
+ passed: failed === 0,
2766
+ total: results.length,
2767
+ failed,
2768
+ vectors: results,
2769
+ };
2770
+ }
2771
+ function acsSlackSnapshot(input) {
2772
+ return {
2773
+ schemaVersion: "axtary.acs_snapshot.v0",
2774
+ actor: {
2775
+ agentId: "agent:codex-prod",
2776
+ humanOwner: "user:asrar@company.com",
2777
+ runtime: "codex-cli",
2778
+ tenant: "org:axtary",
2779
+ },
2780
+ intent: {
2781
+ taskId: "AXT-M9-4",
2782
+ declaredGoal: "Verify ACS pre_tool_call routing through Axtary",
2783
+ },
2784
+ tool_call: {
2785
+ tool: "slack.chat.postMessage",
2786
+ resource: "slack:workspace/company",
2787
+ arguments: {
2788
+ channel: input.channel,
2789
+ message: input.message,
2790
+ ...(input.externalRecipients
2791
+ ? { externalRecipients: input.externalRecipients }
2792
+ : {}),
2793
+ },
2794
+ },
2795
+ };
2796
+ }
2606
2797
  export async function runLedgerExport(input = {}) {
2607
2798
  const loadedConfig = await loadAxtaryConfig({
2608
2799
  filePath: input.configPath,
@@ -2660,7 +2851,7 @@ export async function runLedgerSync(input) {
2660
2851
  tenant: input.tenant ?? loadedConfig.config.tenant,
2661
2852
  token,
2662
2853
  export: exported,
2663
- fetch: input.fetch,
2854
+ fetch: input.fetch ?? globalThis.fetch,
2664
2855
  });
2665
2856
  return {
2666
2857
  schemaVersion: LEDGER_SYNC_RUN_SCHEMA_VERSION,
@@ -2690,7 +2881,7 @@ export async function runLedgerOtelExport(input) {
2690
2881
  serviceName: input.serviceName ?? "axtary-ledger-export",
2691
2882
  timeoutMs: input.timeoutMs,
2692
2883
  records: exported.records,
2693
- fetch: input.fetch,
2884
+ fetch: input.fetch ?? globalThis.fetch,
2694
2885
  });
2695
2886
  return {
2696
2887
  schemaVersion: LEDGER_OTEL_EXPORT_RUN_SCHEMA_VERSION,
@@ -2819,13 +3010,56 @@ export async function runLedgerVerifyExport(input) {
2819
3010
  const cwd = input.cwd ?? process.cwd();
2820
3011
  const bundlePath = resolve(cwd, input.bundlePath);
2821
3012
  const text = await readFile(bundlePath, "utf8");
2822
- const result = await verifyLedgerAttestationBundle(JSON.parse(text), {
3013
+ const parsedBundle = JSON.parse(text);
3014
+ const result = await verifyLedgerAttestationBundle(parsedBundle, {
2823
3015
  issuer: input.issuer,
2824
3016
  });
3017
+ const equivalence = result.valid
3018
+ ? evaluateLedgerEquivalence(LedgerAttestationBundleSchema.parse(parsedBundle).export.records)
3019
+ : null;
2825
3020
  return {
2826
3021
  schemaVersion: LEDGER_VERIFY_EXPORT_RUN_SCHEMA_VERSION,
2827
3022
  bundlePath,
2828
3023
  result,
3024
+ equivalence,
3025
+ };
3026
+ }
3027
+ /**
3028
+ * `axtary prove-equivalence` — the queryable, exportable approval↔execution
3029
+ * equivalence proof. Reads the ledger through the fail-closed export path
3030
+ * (a tampered chain refuses to export at all), then reports, per execution
3031
+ * record, the approved payload hash, the executed payload hash, and whether
3032
+ * they are equal. Executions without approval evidence report `unproven`,
3033
+ * never fake-proved.
3034
+ */
3035
+ export async function runLedgerProveEquivalence(input = {}) {
3036
+ const loadedConfig = await loadAxtaryConfig({
3037
+ filePath: input.configPath,
3038
+ cwd: input.cwd,
3039
+ });
3040
+ const cwd = input.cwd ?? process.cwd();
3041
+ const ledgerPath = resolveConfiguredLedgerPath(input, loadedConfig);
3042
+ const exported = await exportLedgerRecords({
3043
+ filePath: ledgerPath,
3044
+ from: input.from,
3045
+ to: input.to,
3046
+ decisions: input.decisions,
3047
+ });
3048
+ const report = evaluateLedgerEquivalence(exported.records);
3049
+ const outputPath = input.outputPath ? resolve(cwd, input.outputPath) : null;
3050
+ if (outputPath) {
3051
+ await mkdir(dirname(outputPath), { recursive: true });
3052
+ await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
3053
+ }
3054
+ return {
3055
+ schemaVersion: LEDGER_PROVE_EQUIVALENCE_RUN_SCHEMA_VERSION,
3056
+ config: {
3057
+ filePath: loadedConfig.filePath,
3058
+ tenant: loadedConfig.config.tenant,
3059
+ },
3060
+ ledgerPath,
3061
+ outputPath,
3062
+ report,
2829
3063
  };
2830
3064
  }
2831
3065
  async function readAttestationBundle(cwd, bundlePath) {
@@ -3374,6 +3608,7 @@ export async function runRealWorkflow(input = {}) {
3374
3608
  mutation: tamperSetup.mutation,
3375
3609
  approvedPayloadHash: tamperSetup.approvedPayloadHash,
3376
3610
  attemptedPayloadHash: tamperSetup.attemptedPayloadHash,
3611
+ scopedAuthority: tamperSetup.scopedAuthority,
3377
3612
  reached: tamperStep !== null,
3378
3613
  blocked: tamperStep?.status === "blocked",
3379
3614
  blockReasons: tamperStep?.reasons ?? [],
@@ -3989,6 +4224,15 @@ export async function runAxtaryCli(argv = process.argv.slice(2), io = {}) {
3989
4224
  stdout(helpText());
3990
4225
  return 0;
3991
4226
  }
4227
+ if (command === "--version" || command === "-v" || command === "version") {
4228
+ stdout(`${cliPackageVersion()}\n`);
4229
+ return 0;
4230
+ }
4231
+ const commandHelp = commandHelpText(command, args);
4232
+ if (commandHelp) {
4233
+ stdout(commandHelp);
4234
+ return 0;
4235
+ }
3992
4236
  if (command === "init") {
3993
4237
  const parsed = parseInitArgs(args);
3994
4238
  const result = await runInit({
@@ -4046,6 +4290,8 @@ export async function runAxtaryCli(argv = process.argv.slice(2), io = {}) {
4046
4290
  configPath: parsed.configPath,
4047
4291
  ledgerPath: parsed.ledgerPath,
4048
4292
  cwd: io.cwd ?? parsed.cwd,
4293
+ approveStepUp: parsed.approveStepUp,
4294
+ approvedBy: parsed.approvedBy,
4049
4295
  });
4050
4296
  if (parsed.json) {
4051
4297
  stdout(`${JSON.stringify(result, null, 2)}\n`);
@@ -4230,6 +4476,25 @@ export async function runAxtaryCli(argv = process.argv.slice(2), io = {}) {
4230
4476
  }
4231
4477
  return result.result.valid ? 0 : 1;
4232
4478
  }
4479
+ if (command === "prove-equivalence") {
4480
+ const parsed = parseProveEquivalenceArgs(args);
4481
+ const result = await runLedgerProveEquivalence({
4482
+ configPath: parsed.configPath,
4483
+ ledgerPath: parsed.ledgerPath,
4484
+ cwd: io.cwd ?? parsed.cwd,
4485
+ from: parsed.from,
4486
+ to: parsed.to,
4487
+ decisions: parsed.decisions,
4488
+ outputPath: parsed.outputPath,
4489
+ });
4490
+ if (parsed.json) {
4491
+ stdout(`${JSON.stringify(result, null, 2)}\n`);
4492
+ }
4493
+ else {
4494
+ stdout(formatProveEquivalenceResult(result));
4495
+ }
4496
+ return result.report.failed === 0 ? 0 : 1;
4497
+ }
4233
4498
  if (command === "prove-inclusion") {
4234
4499
  const parsed = parseProveInclusionArgs(args);
4235
4500
  const result = await runLedgerProveInclusion({
@@ -4711,6 +4976,21 @@ export async function runAxtaryCli(argv = process.argv.slice(2), io = {}) {
4711
4976
  }
4712
4977
  return result.passed ? 0 : 1;
4713
4978
  }
4979
+ if (command === "acs") {
4980
+ const parsed = parseAcsArgs(args);
4981
+ const result = await runAcsConformance({
4982
+ configPath: parsed.configPath,
4983
+ ledgerPath: parsed.ledgerPath,
4984
+ cwd: io.cwd ?? parsed.cwd,
4985
+ });
4986
+ if (parsed.json) {
4987
+ stdout(`${JSON.stringify(result, null, 2)}\n`);
4988
+ }
4989
+ else {
4990
+ stdout(formatAcsConformanceResult(result));
4991
+ }
4992
+ return result.passed ? 0 : 1;
4993
+ }
4714
4994
  if (command === "connect") {
4715
4995
  const parsed = parseConnectArgs(args);
4716
4996
  const result = await runConnect({
@@ -5627,6 +5907,39 @@ function parseTestPolicyArgs(args) {
5627
5907
  }
5628
5908
  return parsed;
5629
5909
  }
5910
+ function parseAcsArgs(args) {
5911
+ const subcommand = args[0];
5912
+ if (subcommand !== "conformance") {
5913
+ throw new Error("acs_requires_conformance");
5914
+ }
5915
+ const parsed = {
5916
+ subcommand,
5917
+ json: false,
5918
+ };
5919
+ for (let index = 1; index < args.length; index += 1) {
5920
+ const arg = args[index];
5921
+ switch (arg) {
5922
+ case "--config":
5923
+ parsed.configPath = requireValue(args, index, "--config");
5924
+ index += 1;
5925
+ break;
5926
+ case "--ledger":
5927
+ parsed.ledgerPath = requireValue(args, index, "--ledger");
5928
+ index += 1;
5929
+ break;
5930
+ case "--cwd":
5931
+ parsed.cwd = requireValue(args, index, "--cwd");
5932
+ index += 1;
5933
+ break;
5934
+ case "--json":
5935
+ parsed.json = true;
5936
+ break;
5937
+ default:
5938
+ throw new Error(`unknown_acs_flag:${arg}`);
5939
+ }
5940
+ }
5941
+ return parsed;
5942
+ }
5630
5943
  function parseSmokeArgs(args) {
5631
5944
  const parsed = {};
5632
5945
  for (let index = 0; index < args.length; index += 1) {
@@ -6000,6 +6313,51 @@ function parseLedgerAttestArgs(args) {
6000
6313
  }
6001
6314
  return parsed;
6002
6315
  }
6316
+ function parseProveEquivalenceArgs(args) {
6317
+ const parsed = {
6318
+ json: false,
6319
+ decisions: [],
6320
+ };
6321
+ for (let index = 0; index < args.length; index += 1) {
6322
+ const arg = args[index];
6323
+ switch (arg) {
6324
+ case "--config":
6325
+ parsed.configPath = requireValue(args, index, "--config");
6326
+ index += 1;
6327
+ break;
6328
+ case "--ledger":
6329
+ parsed.ledgerPath = requireValue(args, index, "--ledger");
6330
+ index += 1;
6331
+ break;
6332
+ case "--cwd":
6333
+ parsed.cwd = requireValue(args, index, "--cwd");
6334
+ index += 1;
6335
+ break;
6336
+ case "--from":
6337
+ parsed.from = requireValue(args, index, "--from");
6338
+ index += 1;
6339
+ break;
6340
+ case "--to":
6341
+ parsed.to = requireValue(args, index, "--to");
6342
+ index += 1;
6343
+ break;
6344
+ case "--decision":
6345
+ parsed.decisions.push(parseDecisionFlag(requireValue(args, index, "--decision")));
6346
+ index += 1;
6347
+ break;
6348
+ case "--out":
6349
+ parsed.outputPath = requireValue(args, index, "--out");
6350
+ index += 1;
6351
+ break;
6352
+ case "--json":
6353
+ parsed.json = true;
6354
+ break;
6355
+ default:
6356
+ throw new Error(`unknown_prove_equivalence_flag:${arg}`);
6357
+ }
6358
+ }
6359
+ return parsed;
6360
+ }
6003
6361
  function parseVerifyExportArgs(args) {
6004
6362
  const parsed = { bundlePath: "", json: false };
6005
6363
  for (let index = 0; index < args.length; index += 1) {
@@ -6328,6 +6686,13 @@ function parseDemoArgs(args) {
6328
6686
  parsed.templateId = requireValue(args, index, "--template");
6329
6687
  index += 1;
6330
6688
  break;
6689
+ case "--approve-step-up":
6690
+ parsed.approveStepUp = true;
6691
+ break;
6692
+ case "--approved-by":
6693
+ parsed.approvedBy = requireValue(args, index, "--approved-by");
6694
+ index += 1;
6695
+ break;
6331
6696
  case "--json":
6332
6697
  parsed.json = true;
6333
6698
  break;
@@ -6536,6 +6901,14 @@ function prepareTamperedSlackStep(actions, approvedBy) {
6536
6901
  approvedPayloadHash: artifact.payloadHash,
6537
6902
  attemptedPayloadHash: hashPayload(tamperedPayload),
6538
6903
  mutation: "Slack message payload mutated after exact approval while reusing the original approval artifact.",
6904
+ scopedAuthority: {
6905
+ tool: target.action.capability.tool,
6906
+ resource: target.action.capability.resource,
6907
+ approvedBy: artifact.approvedBy,
6908
+ artifactId: artifact.id,
6909
+ scopeUnchanged: target.action.capability.tool === approvedAction.capability.tool &&
6910
+ target.action.capability.resource === approvedAction.capability.resource,
6911
+ },
6539
6912
  };
6540
6913
  }
6541
6914
  function assertRealWorkflowModes(loadedConfig) {
@@ -6581,8 +6954,8 @@ async function doctorProvider(input) {
6581
6954
  requiredScopes: input.requiredScopes,
6582
6955
  smokeCheck: input.smokeCheck,
6583
6956
  summary: input.capability === "fake"
6584
- ? "fake mode configured; no real provider credentials are used"
6585
- : "connector is off",
6957
+ ? "credential-free demo mode; no provider calls are made"
6958
+ : "implemented connector is disabled in this config",
6586
6959
  };
6587
6960
  }
6588
6961
  const missingEnv = input.requiredEnv.filter((name) => !input.env[name]);
@@ -6658,7 +7031,7 @@ function formatDemoResult(result) {
6658
7031
  rows: [
6659
7032
  ["ledger", `${ledgerField(result.ledger)} · ${result.ledgerPath}`],
6660
7033
  [
6661
- "fake side effects",
7034
+ "demo side effects",
6662
7035
  `${result.adapters.github.pullRequests.length} GitHub PR, ${result.adapters.slack.messages.length} Slack, ${result.adapters.issueTrackers.linear.comments.length} Linear, ${result.adapters.issueTrackers.jira.comments.length} Jira`,
6663
7036
  ],
6664
7037
  ],
@@ -6796,6 +7169,36 @@ function formatPolicyParityResult(result) {
6796
7169
  ["fixtures", result.fixturesPath],
6797
7170
  ["cedar", result.cedarPath],
6798
7171
  ["rego", result.regoWasmPath],
7172
+ [
7173
+ "agentcore",
7174
+ `${result.agentCore.status} · ${result.agentCore.fixtureCount} Cedar-shaped Gateway requests`,
7175
+ ],
7176
+ ],
7177
+ });
7178
+ }
7179
+ function formatAcsConformanceResult(result) {
7180
+ return renderWorkflow({
7181
+ command: "acs conformance",
7182
+ state: result.passed ? "ok" : "failed",
7183
+ verdict: result.passed
7184
+ ? `All ${result.total} ACS intervention vectors passed`
7185
+ : `${result.failed} of ${result.total} ACS intervention vectors failed`,
7186
+ steps: result.vectors.map((entry) => ({
7187
+ state: entry.passed ? "ok" : "failed",
7188
+ name: entry.id,
7189
+ detail: `${entry.interventionPoint} -> ${entry.actualVerdict}` +
7190
+ ` · proxy ${entry.actualProxyStatus ?? "none"}` +
7191
+ ` · ${entry.reasons.join(", ")}`,
7192
+ })),
7193
+ rows: [
7194
+ ["manifest", result.manifestVersion],
7195
+ ["ledger", result.ledgerPath],
7196
+ [
7197
+ "actionpass",
7198
+ result.vectors.some((entry) => entry.actionPassId)
7199
+ ? "issued for allow vector"
7200
+ : "not issued",
7201
+ ],
6799
7202
  ],
6800
7203
  });
6801
7204
  }
@@ -6850,17 +7253,41 @@ function formatLedgerAttestResult(result) {
6850
7253
  ],
6851
7254
  });
6852
7255
  }
7256
+ /**
7257
+ * One line summarizing the approval↔execution equivalence over a set of
7258
+ * execution records. Verification fails closed on any failed pair, so a
7259
+ * summary rendered for a VALID export can only contain proved + unproven.
7260
+ */
7261
+ export function equivalenceSummaryLine(report) {
7262
+ if (report.executions === 0) {
7263
+ return "no execution records";
7264
+ }
7265
+ const parts = [
7266
+ `${report.proved} of ${report.executions} execution${report.executions === 1 ? "" : "s"} proved approved == executed`,
7267
+ ];
7268
+ if (report.unproven > 0) {
7269
+ parts.push(`${report.unproven} without approval evidence (unproven)`);
7270
+ }
7271
+ if (report.failed > 0) {
7272
+ parts.push(`${report.failed} FAILED`);
7273
+ }
7274
+ return parts.join(" · ");
7275
+ }
6853
7276
  function formatVerifyExportResult(result) {
6854
7277
  if (result.result.valid) {
7278
+ const rows = [
7279
+ ["bundle", result.bundlePath],
7280
+ ["issuer", result.result.claims.iss],
7281
+ ["export digest", shortHash(result.result.recomputedDigest)],
7282
+ ];
7283
+ if (result.equivalence) {
7284
+ rows.push(["equivalence", equivalenceSummaryLine(result.equivalence)]);
7285
+ }
6855
7286
  return renderWorkflow({
6856
7287
  command: "verify-export",
6857
7288
  state: "ok",
6858
7289
  verdict: `Export VALID · ${result.result.claims.recordCount} record${result.result.claims.recordCount === 1 ? "" : "s"} independently verified`,
6859
- rows: [
6860
- ["bundle", result.bundlePath],
6861
- ["issuer", result.result.claims.iss],
6862
- ["export digest", shortHash(result.result.recomputedDigest)],
6863
- ],
7290
+ rows,
6864
7291
  });
6865
7292
  }
6866
7293
  return renderWorkflow({
@@ -6873,6 +7300,35 @@ function formatVerifyExportResult(result) {
6873
7300
  ],
6874
7301
  });
6875
7302
  }
7303
+ function formatProveEquivalenceResult(result) {
7304
+ const report = result.report;
7305
+ const failed = report.entries.filter((entry) => entry.status === "failed");
7306
+ const stepRows = failed.map((entry) => ({
7307
+ state: "failed",
7308
+ name: `record ${entry.line}${entry.tool ? ` (${entry.tool})` : ""}`,
7309
+ detail: `${entry.failure} · approved ${entry.approvedPayloadHash ? shortHash(entry.approvedPayloadHash) : "absent"} vs executed ${entry.executedPayloadHash ? shortHash(entry.executedPayloadHash) : "absent"}`,
7310
+ }));
7311
+ return renderWorkflow({
7312
+ command: "prove-equivalence",
7313
+ state: report.failed > 0 ? "failed" : "ok",
7314
+ verdict: report.failed > 0
7315
+ ? `Equivalence FAILED · ${report.failed} execution record${report.failed === 1 ? "" : "s"} broke approved == executed`
7316
+ : report.executions === 0
7317
+ ? "No execution records to prove · the ledger chain is intact"
7318
+ : report.proved === 0
7319
+ ? `No approved executions to prove · ${report.unproven} execution${report.unproven === 1 ? "" : "s"} without approval evidence · chain intact`
7320
+ : `Equivalence holds · ${equivalenceSummaryLine(report)}`,
7321
+ steps: stepRows,
7322
+ rows: [
7323
+ ["ledger", result.ledgerPath],
7324
+ ["executions", report.executions],
7325
+ ["proved", report.proved],
7326
+ ["unproven", report.unproven],
7327
+ ["failed", report.failed],
7328
+ ...(result.outputPath ? [["out", result.outputPath]] : []),
7329
+ ],
7330
+ });
7331
+ }
6876
7332
  /** Map a per-connector readiness status to a render state. */
6877
7333
  function doctorProviderState(status) {
6878
7334
  if (status === "ready")
@@ -6884,8 +7340,22 @@ function doctorProviderState(status) {
6884
7340
  return "warn"; // missing — needs setup
6885
7341
  }
6886
7342
  function formatConnectorDoctorResult(result) {
6887
- const ready = result.providers.filter((p) => p.status === "ready").length;
6888
- const total = result.providers.length;
7343
+ const configured = result.providers.filter((provider) => provider.status !== "skipped");
7344
+ const ready = configured.filter((provider) => provider.status === "ready").length;
7345
+ const needsSetup = configured.length - ready;
7346
+ const demo = result.providers.filter((provider) => provider.capability === "fake").length;
7347
+ const disabled = result.providers.filter((provider) => provider.capability === "off").length;
7348
+ const summaryParts = [
7349
+ configured.length === 0
7350
+ ? "No live or local connector checks configured"
7351
+ : needsSetup === 0
7352
+ ? `${ready} configured connector check${ready === 1 ? "" : "s"} passed`
7353
+ : `${ready} of ${configured.length} configured connector checks passed · ${needsSetup} need setup`,
7354
+ ...(demo > 0
7355
+ ? [`${demo} in credential-free demo mode`]
7356
+ : []),
7357
+ ...(disabled > 0 ? [`${disabled} disabled`] : []),
7358
+ ];
6889
7359
  const stepRows = result.providers.map((provider) => {
6890
7360
  const extras = [];
6891
7361
  if (provider.missingEnv.length > 0) {
@@ -6899,16 +7369,14 @@ function formatConnectorDoctorResult(result) {
6899
7369
  : provider.summary;
6900
7370
  return {
6901
7371
  state: doctorProviderState(provider.status),
6902
- name: `${provider.provider} (${provider.mode})`,
7372
+ name: `${provider.provider} (${provider.mode === "fake" ? "demo" : provider.mode === "off" ? "disabled" : provider.mode})`,
6903
7373
  detail,
6904
7374
  };
6905
7375
  });
6906
7376
  return renderWorkflow({
6907
7377
  command: "doctor connectors",
6908
7378
  state: result.status === "ready" ? "ok" : "warn",
6909
- verdict: result.status === "ready"
6910
- ? `All ${total} connectors ready`
6911
- : `${ready} of ${total} connectors ready · the rest need setup before real mode`,
7379
+ verdict: summaryParts.join(" · "),
6912
7380
  steps: stepRows,
6913
7381
  rows: [["config", result.config.filePath ?? "default"]],
6914
7382
  });
@@ -6919,6 +7387,20 @@ function formatConnectorDoctorResult(result) {
6919
7387
  * step failed before Slack, so the tamper was never exercised — must never be
6920
7388
  * reported as the tampered payload executing).
6921
7389
  */
7390
+ /**
7391
+ * The scoped-authority contrast line — the differentiation beat spelled out:
7392
+ * the agent still held a valid, exact-payload human approval whose tool +
7393
+ * resource scope matched the attempted call, so an authorizer that checks
7394
+ * scope alone would have let the swapped payload through. Only claim that
7395
+ * when `scopeUnchanged` was actually computed true; otherwise state the
7396
+ * scope changed too, which any authorizer should reject.
7397
+ */
7398
+ export function scopedAuthorityLine(authority) {
7399
+ const held = `valid approval by ${authority.approvedBy} for ${authority.tool} on ${authority.resource}`;
7400
+ return authority.scopeUnchanged
7401
+ ? `${held} · scope unchanged - a scope-only check would allow this call; Axtary binds the approval to the exact payload hash`
7402
+ : `${held} · scope changed by the mutation`;
7403
+ }
6922
7404
  export function tamperOutcomeLine(tamper) {
6923
7405
  if (!tamper.reached) {
6924
7406
  return "tamper step not reached - a prior step failed before Slack, so content binding was not exercised this run (re-run with a fresh --head). The tampered payload did NOT execute.";
@@ -6953,6 +7435,7 @@ function formatRealWorkflowRunResult(result) {
6953
7435
  ? "executed"
6954
7436
  : "failed";
6955
7437
  rows.push(["tamper", `${result.tamper.step} (${result.tamper.tool}) · ${result.tamper.mutation}`]);
7438
+ rows.push(["scoped authority", scopedAuthorityLine(result.tamper.scopedAuthority)]);
6956
7439
  rows.push(["approved hash", shortHash(result.tamper.approvedPayloadHash)]);
6957
7440
  rows.push(["attempted hash", shortHash(result.tamper.attemptedPayloadHash)]);
6958
7441
  rows.push(["tamper outcome", `${stateGlyph(tamperState)} ${tamperOutcomeLine(result.tamper)}`]);
@@ -7345,57 +7828,111 @@ function createProxyHandlers(loadedConfig, adapterState, verification, options =
7345
7828
  adapterModes,
7346
7829
  };
7347
7830
  }
7831
+ /**
7832
+ * The CLI's own published version, read from its package manifest at runtime
7833
+ * so `axtary --version` can never drift from what npm actually installed.
7834
+ */
7835
+ function cliPackageVersion() {
7836
+ const manifestUrl = new URL("../package.json", import.meta.url);
7837
+ const manifest = JSON.parse(readFileSync(manifestUrl, "utf8"));
7838
+ return manifest.version ?? "unknown";
7839
+ }
7840
+ const DETAILED_USAGE_LINES = [
7841
+ "Usage:",
7842
+ " axtary init [--template repo-only-coding|staging-reads|incident-investigation|ticket-updates|doc-search|guarded-prod] [--config axtary.yml] [--force] [--json] (scaffold a starter config and optional scope-template harness)",
7843
+ " axtary demo [--config axtary.yml] [--template <name>] [--ledger .axtary/actions.jsonl] [--approve-step-up [--approved-by user:you@example.com]] [--json] (--approve-step-up attaches a local exact-payload approval so step-up actions execute with the approved/executed hash pair recorded)",
7844
+ " axtary revoke <actionpass-jti> [--trust-store .axtary/actionpass-trust-store.json] [--status-store .axtary/actionpass-status.json] [--by user:you@example.com] [--reason 'task cancelled'] [--json]",
7845
+ " axtary revocations [--trust-store .axtary/actionpass-trust-store.json] [--json]",
7846
+ " axtary issuer-keys [--keyring .axtary/keys/actionpass-issuer-keyring.json] [--rotate] [--retire-after 1200] (metadata + public JWKS only; never prints private keys)",
7847
+ " axtary status [--store .axtary/actionpass-status.json] (local ActionPass status-list allocation/state)",
7848
+ " axtary caep-map --issuer https://idp.example --subject '{\"format\":\"opaque\",\"id\":\"session\"}' --root ap_... [--root ap_...] (map an SSF subject to live delegation roots)",
7849
+ " axtary doctor connectors [--config axtary.yml] [--json]",
7850
+ " axtary connect linear [--client-id <id> | --client-id-env NAME] [--client-secret-env NAME] [--redirect-port 7878] [--json] (browser OAuth via http loopback; token stored in the local credential broker, never pasted)",
7851
+ " axtary connect slack --redirect-uri https://<tunnel>/callback ... (Slack rejects http loopback; needs an HTTPS redirect — or keep SLACK_BOT_TOKEN in env)",
7852
+ " axtary connect jira --redirect-uri https://<tunnel>/callback [--cloud-id ID] ... (Atlassian 3LO + site discovery + rotating refresh token in the local broker)",
7853
+ " axtary connect drive --redirect-uri https://<tunnel>/callback ... (Google desktop Picker + drive.file only; stores the selected file id and OAuth token locally)",
7854
+ " axtary connections [--json] (list connected providers — metadata only, never secrets)",
7855
+ " axtary disconnect <provider> (remove a stored credential)",
7856
+ " axtary run workflow github-pr-review --real --config examples/axtary.real.yml --repo owner/repo --linear-issue AXT-418 --slack-channel '#axtary-dev' [--approve-step-up | --hosted-approval --approval-endpoint https://app.example [--approval-token-env AXTARY_APPROVAL_TOKEN] [--approval-poll-interval 3] [--approval-timeout 600]] [--tamper] [--approved-by user:you@example.com] [--ledger .axtary/actions.jsonl] [--json]",
7857
+ " axtary run workflow github-depth --real --config examples/axtary.real.yml --repo owner/repo [--linear-issue AXT-418] [--head axtary/axt-418-github-depth] [--check-name axtary/governed-check] [--review-line 1] [--approve-step-up] [--approved-by user:you@example.com] [--ledger .axtary/actions.jsonl] [--json] (GitHub-only: opens a draft PR then writes an inline review comment, a check run, and an issue comment, all governed + ledgered)",
7858
+ " axtary run workflow postgres-read --real --config examples/axtary.postgres.yml --database appdb --statement 'SELECT id FROM public.items WHERE tenant_id = $1' --parameters '[\"tenant-a\"]' [--max-rows 100] [--approve-step-up] [--ledger .axtary/postgres-actions.jsonl] [--json] (Postgres-only: one parameterized SELECT through policy, ActionPass, read-only transaction, RLS checks, and ledger)",
7859
+ " axtary run workflow drive-read --real --config examples/axtary.drive.yml [--file-id GOOGLE_FILE_ID] [--max-bytes 100000] [--ledger .axtary/drive-actions.jsonl] [--json] (Google Drive-only: reads one Picker-authorized drive.file resource through policy, ActionPass, adapter, and ledger)",
7860
+ " axtary export-ledger [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--from ISO] [--to ISO] [--decision allow|deny|step_up] [--format json|jsonl|siem-jsonl] [--out export.json] [--json]",
7861
+ " axtary sync-ledger --endpoint https://app.example/api/ledger/sync [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--from ISO] [--to ISO] [--decision allow|deny|step_up] [--tenant org:company] [--token-env AXTARY_LEDGER_SYNC_TOKEN] [--json]",
7862
+ " axtary export-otel --endpoint http://127.0.0.1:4318/v1/traces [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--from ISO] [--to ISO] [--decision allow|deny|step_up] [--service-name axtary-local] [--json] # opt-in OTLP/HTTP GenAI execute_tool spans, payload-free",
7863
+ " axtary attest-ledger [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--from ISO] [--to ISO] [--decision allow|deny|step_up] [--out attestation.json] [--key .axtary/keys/attestation-key.json] [--json]",
7864
+ " axtary verify-export <attestation.json> [--issuer https://local.axtary.dev] [--json] # standalone: signature + chain + digest + approval↔execution equivalence",
7865
+ " axtary prove-equivalence [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--from ISO] [--to ISO] [--decision allow|deny|step_up] [--out equivalence.json] [--json] # per-execution approved_payload_hash == executed_payload_hash proof, exportable",
7866
+ " axtary prove-inclusion <attestation.json> (--record <jti> | --index <n>) [--out proof.json] [--json] # RFC 6962 inclusion proof",
7867
+ " axtary prove-consistency <first.json> <second.json> [--out proof.json] [--json] # RFC 6962 consistency proof across two heads",
7868
+ " axtary verify-proof <proof.json> (--bundle <b.json> | --first <a.json> --second <b.json>) [--issuer ...] [--json] # standalone proof verifier",
7869
+ " axtary forensics [<attestation.json>] [--ledger .axtary/actions.jsonl] [--json] # offline incident reconstruction: attenuation, reconstructibility, cascade containment",
7870
+ " axtary proxy [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--trust-store .axtary/actionpass-trust-store.json] [--host 127.0.0.1] [--port 7331] [--otlp-endpoint http://127.0.0.1:4318/v1/traces]",
7871
+ " axtary hook install <claude|cursor|codex> [--proxy http://127.0.0.1:7331] [--repo owner/repo] [--owner user:you@example.com] [--branch main] [--timeout 5000] [--cwd path] [--json] (write an idempotent project hook config with resolved CLI paths)",
7872
+ " axtary hook claude-code [--proxy http://127.0.0.1:7331] [--repo owner/repo] [--owner user:you@example.com] [--branch main] [--timeout 5000] (reads a Claude Code PreToolUse payload from stdin)",
7873
+ " axtary hook cursor [--proxy http://127.0.0.1:7331] [--repo owner/repo] [--owner user:you@example.com] [--branch main] [--timeout 5000] (reads a Cursor beforeMCPExecution payload from stdin; emits {permission,user_message,agent_message})",
7874
+ " axtary hook codex [--proxy http://127.0.0.1:7331] [--repo owner/repo] [--owner user:you@example.com] [--branch main] [--timeout 5000] (reads a Codex PreToolUse payload from stdin; gates file-touching shell commands, apply_patch, and MCP fs tools)",
7875
+ " axtary mcp serve [--config axtary.yml] [--wrap 'npx -y @modelcontextprotocol/server-everything' | --wrap-url https://host/mcp [--header 'Authorization: Bearer …'] [--oauth]] [--owner user:you@example.com] [--ledger .axtary/actions.jsonl] [--trust-store .axtary/actionpass-trust-store.json] [--pins axtary.mcp-pins.json] [--strict] (stdio or Streamable-HTTP MCP server; wrapped tools are policy-gated, pass-signed, revocation-checked, ledger-recorded, and pinned by definition hash; --oauth resolves the local OAuth bearer)",
7876
+ " axtary mcp login --wrap-url https://host/mcp [--scope 'a b'] [--client-id id] [--redirect-port 7878] [--json] (OAuth-2.1 discovery + PKCE consent for an OAuth-protected remote MCP server; bearer stored in the local broker, never printed)",
7877
+ " axtary mcp sessions [--json] (list remote MCP OAuth sessions from the local broker — resource/issuer/scopes/expiry, never tokens)",
7878
+ " axtary mcp logout --wrap-url https://host/mcp [--json] (delete the locally stored OAuth session for a remote MCP server)",
7879
+ " axtary mcp pins [--pins axtary.mcp-pins.json] [--json] (list the persisted tool-definition pins)",
7880
+ " axtary mcp trust-publisher --publisher mcp-publisher://acme --jwks publisher.jwks.json [--publisher-trust .axtary/mcp-publisher-trust.json] (register public publisher keys; no TOFU)",
7881
+ " axtary mcp publishers [--publisher-trust .axtary/mcp-publisher-trust.json] [--json] (list trusted publisher identities and public key ids)",
7882
+ " axtary mcp verify-definition --definition tool.json --statement tool.jwt --publisher mcp-publisher://acme [--publisher-trust path] [--json] (verify publisher signature against the exact definition)",
7883
+ " axtary mcp review (--wrap 'npx -y <server>' | --wrap-url https://host/mcp [--header 'K: V'] [--oauth]) [--pins axtary.mcp-pins.json] [--publisher-trust path] [--signed-definitions registry.json] [--accept] [--json] (diff pins and surface verified publisher/version-chain evidence)",
7884
+ " axtary mcp conformance (--wrap 'npx -y <server>' | --wrap-url https://host/mcp [--header 'K: V'] [--oauth]) --tool <name> --arguments '<json-object>' [--accept] [--pins path] [--publisher-trust path] [--signed-definitions registry.json] [--ledger path] [--status path] [--attestation path] [--key path] [--json] (live governed call + publisher/pin evidence + v1/DPoP ledger + drift quarantine + attestation)",
7885
+ " axtary mcp drift-demo [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--json] (reproducible tool-poisoning block: definition mutates after review, hash drift denies the call)",
7886
+ " axtary smoke [--config axtary.yml]",
7887
+ " axtary policy simulate <action.json> [--config axtary.yml] [--json] (dry-run rule matches, decision, and obligations)",
7888
+ " axtary policy test <policies.test.yml> [--config axtary.yml] [--json] (CI policy harness)",
7889
+ " axtary test-policy --fixtures ./fixtures [--config axtary.yml] [--json]",
7890
+ " axtary test-policy --parity [--fixtures parity-fixtures.json] [--cedar <file.cedar>] [--rego-wasm <policy.wasm>] [--json]",
7891
+ " axtary acs conformance [--config axtary.yml] [--ledger .axtary/acs-conformance.jsonl] [--json] (credential-free ACS pre_tool_call vectors through Axtary policy, ActionPass, and ledger)",
7892
+ " axtary --version (print the installed CLI version)",
7893
+ ];
7348
7894
  function helpText() {
7349
7895
  return [
7896
+ "Axtary — content authorization for AI-agent actions",
7897
+ "",
7350
7898
  "Usage:",
7351
- " axtary init [--template repo-only-coding|staging-reads|incident-investigation|ticket-updates|doc-search|guarded-prod] [--config axtary.yml] [--force] [--json] (scaffold a starter config and optional scope-template harness)",
7352
- " axtary demo [--config axtary.yml] [--template <name>] [--ledger .axtary/actions.jsonl] [--json]",
7353
- " axtary revoke <actionpass-jti> [--trust-store .axtary/actionpass-trust-store.json] [--status-store .axtary/actionpass-status.json] [--by user:you@example.com] [--reason 'task cancelled'] [--json]",
7354
- " axtary revocations [--trust-store .axtary/actionpass-trust-store.json] [--json]",
7355
- " axtary issuer-keys [--keyring .axtary/keys/actionpass-issuer-keyring.json] [--rotate] [--retire-after 1200] (metadata + public JWKS only; never prints private keys)",
7356
- " axtary status [--store .axtary/actionpass-status.json] (local ActionPass status-list allocation/state)",
7357
- " axtary caep-map --issuer https://idp.example --subject '{\"format\":\"opaque\",\"id\":\"session\"}' --root ap_... [--root ap_...] (map an SSF subject to live delegation roots)",
7358
- " axtary doctor connectors [--config axtary.yml] [--json]",
7359
- " axtary connect linear [--client-id <id> | --client-id-env NAME] [--client-secret-env NAME] [--redirect-port 7878] [--json] (browser OAuth via http loopback; token stored in the local credential broker, never pasted)",
7360
- " axtary connect slack --redirect-uri https://<tunnel>/callback ... (Slack rejects http loopback; needs an HTTPS redirect — or keep SLACK_BOT_TOKEN in env)",
7361
- " axtary connect jira --redirect-uri https://<tunnel>/callback [--cloud-id ID] ... (Atlassian 3LO + site discovery + rotating refresh token in the local broker)",
7362
- " axtary connect drive --redirect-uri https://<tunnel>/callback ... (Google desktop Picker + drive.file only; stores the selected file id and OAuth token locally)",
7363
- " axtary connections [--json] (list connected providers metadata only, never secrets)",
7364
- " axtary disconnect <provider> (remove a stored credential)",
7365
- " axtary run workflow github-pr-review --real --config examples/axtary.real.yml --repo owner/repo --linear-issue AXT-418 --slack-channel '#axtary-dev' [--approve-step-up | --hosted-approval --approval-endpoint https://app.example [--approval-token-env AXTARY_APPROVAL_TOKEN] [--approval-poll-interval 3] [--approval-timeout 600]] [--tamper] [--approved-by user:you@example.com] [--ledger .axtary/actions.jsonl] [--json]",
7366
- " axtary run workflow github-depth --real --config examples/axtary.real.yml --repo owner/repo [--linear-issue AXT-418] [--head axtary/axt-418-github-depth] [--check-name axtary/governed-check] [--review-line 1] [--approve-step-up] [--approved-by user:you@example.com] [--ledger .axtary/actions.jsonl] [--json] (GitHub-only: opens a draft PR then writes an inline review comment, a check run, and an issue comment, all governed + ledgered)",
7367
- " axtary run workflow postgres-read --real --config examples/axtary.postgres.yml --database appdb --statement 'SELECT id FROM public.items WHERE tenant_id = $1' --parameters '[\"tenant-a\"]' [--max-rows 100] [--approve-step-up] [--ledger .axtary/postgres-actions.jsonl] [--json] (Postgres-only: one parameterized SELECT through policy, ActionPass, read-only transaction, RLS checks, and ledger)",
7368
- " axtary run workflow drive-read --real --config examples/axtary.drive.yml [--file-id GOOGLE_FILE_ID] [--max-bytes 100000] [--ledger .axtary/drive-actions.jsonl] [--json] (Google Drive-only: reads one Picker-authorized drive.file resource through policy, ActionPass, adapter, and ledger)",
7369
- " axtary export-ledger [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--from ISO] [--to ISO] [--decision allow|deny|step_up] [--format json|jsonl|siem-jsonl] [--out export.json] [--json]",
7370
- " axtary sync-ledger --endpoint https://app.example/api/ledger/sync [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--from ISO] [--to ISO] [--decision allow|deny|step_up] [--tenant org:company] [--token-env AXTARY_LEDGER_SYNC_TOKEN] [--json]",
7371
- " axtary export-otel --endpoint http://127.0.0.1:4318/v1/traces [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--from ISO] [--to ISO] [--decision allow|deny|step_up] [--service-name axtary-local] [--json] # opt-in OTLP/HTTP GenAI execute_tool spans, payload-free",
7372
- " axtary attest-ledger [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--from ISO] [--to ISO] [--decision allow|deny|step_up] [--out attestation.json] [--key .axtary/keys/attestation-key.json] [--json]",
7373
- " axtary verify-export <attestation.json> [--issuer https://local.axtary.dev] [--json] # standalone: signature + chain + digest",
7374
- " axtary prove-inclusion <attestation.json> (--record <jti> | --index <n>) [--out proof.json] [--json] # RFC 6962 inclusion proof",
7375
- " axtary prove-consistency <first.json> <second.json> [--out proof.json] [--json] # RFC 6962 consistency proof across two heads",
7376
- " axtary verify-proof <proof.json> (--bundle <b.json> | --first <a.json> --second <b.json>) [--issuer ...] [--json] # standalone proof verifier",
7377
- " axtary forensics [<attestation.json>] [--ledger .axtary/actions.jsonl] [--json] # offline incident reconstruction: attenuation, reconstructibility, cascade containment",
7378
- " axtary proxy [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--trust-store .axtary/actionpass-trust-store.json] [--host 127.0.0.1] [--port 7331] [--otlp-endpoint http://127.0.0.1:4318/v1/traces]",
7379
- " axtary hook install <claude|cursor|codex> [--proxy http://127.0.0.1:7331] [--repo owner/repo] [--owner user:you@example.com] [--branch main] [--timeout 5000] [--cwd path] [--json] (write an idempotent project hook config with resolved CLI paths)",
7380
- " axtary hook claude-code [--proxy http://127.0.0.1:7331] [--repo owner/repo] [--owner user:you@example.com] [--branch main] [--timeout 5000] (reads a Claude Code PreToolUse payload from stdin)",
7381
- " axtary hook cursor [--proxy http://127.0.0.1:7331] [--repo owner/repo] [--owner user:you@example.com] [--branch main] [--timeout 5000] (reads a Cursor beforeMCPExecution payload from stdin; emits {permission,user_message,agent_message})",
7382
- " axtary hook codex [--proxy http://127.0.0.1:7331] [--repo owner/repo] [--owner user:you@example.com] [--branch main] [--timeout 5000] (reads a Codex PreToolUse payload from stdin; gates file-touching shell commands, apply_patch, and MCP fs tools)",
7383
- " axtary mcp serve [--config axtary.yml] [--wrap 'npx -y @modelcontextprotocol/server-everything' | --wrap-url https://host/mcp [--header 'Authorization: Bearer …'] [--oauth]] [--owner user:you@example.com] [--ledger .axtary/actions.jsonl] [--trust-store .axtary/actionpass-trust-store.json] [--pins axtary.mcp-pins.json] [--strict] (stdio or Streamable-HTTP MCP server; wrapped tools are policy-gated, pass-signed, revocation-checked, ledger-recorded, and pinned by definition hash; --oauth resolves the local OAuth bearer)",
7384
- " axtary mcp login --wrap-url https://host/mcp [--scope 'a b'] [--client-id id] [--redirect-port 7878] [--json] (OAuth-2.1 discovery + PKCE consent for an OAuth-protected remote MCP server; bearer stored in the local broker, never printed)",
7385
- " axtary mcp sessions [--json] (list remote MCP OAuth sessions from the local broker — resource/issuer/scopes/expiry, never tokens)",
7386
- " axtary mcp logout --wrap-url https://host/mcp [--json] (delete the locally stored OAuth session for a remote MCP server)",
7387
- " axtary mcp pins [--pins axtary.mcp-pins.json] [--json] (list the persisted tool-definition pins)",
7388
- " axtary mcp trust-publisher --publisher mcp-publisher://acme --jwks publisher.jwks.json [--publisher-trust .axtary/mcp-publisher-trust.json] (register public publisher keys; no TOFU)",
7389
- " axtary mcp publishers [--publisher-trust .axtary/mcp-publisher-trust.json] [--json] (list trusted publisher identities and public key ids)",
7390
- " axtary mcp verify-definition --definition tool.json --statement tool.jwt --publisher mcp-publisher://acme [--publisher-trust path] [--json] (verify publisher signature against the exact definition)",
7391
- " axtary mcp review (--wrap 'npx -y <server>' | --wrap-url https://host/mcp [--header 'K: V'] [--oauth]) [--pins axtary.mcp-pins.json] [--publisher-trust path] [--signed-definitions registry.json] [--accept] [--json] (diff pins and surface verified publisher/version-chain evidence)",
7392
- " axtary mcp conformance (--wrap 'npx -y <server>' | --wrap-url https://host/mcp [--header 'K: V'] [--oauth]) --tool <name> --arguments '<json-object>' [--accept] [--pins path] [--publisher-trust path] [--signed-definitions registry.json] [--ledger path] [--status path] [--attestation path] [--key path] [--json] (live governed call + publisher/pin evidence + v1/DPoP ledger + drift quarantine + attestation)",
7393
- " axtary mcp drift-demo [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--json] (reproducible tool-poisoning block: definition mutates after review, hash drift denies the call)",
7394
- " axtary smoke [--config axtary.yml]",
7395
- " axtary policy simulate <action.json> [--config axtary.yml] [--json] (dry-run rule matches, decision, and obligations)",
7396
- " axtary policy test <policies.test.yml> [--config axtary.yml] [--json] (CI policy harness)",
7397
- " axtary test-policy --fixtures ./fixtures [--config axtary.yml] [--json]",
7398
- " axtary test-policy --parity [--fixtures parity-fixtures.json] [--cedar <file.cedar>] [--rego-wasm <policy.wasm>] [--json]",
7899
+ " axtary <command> [options]",
7900
+ "",
7901
+ "Get started:",
7902
+ " init · doctor · demo · proxy · smoke",
7903
+ "",
7904
+ "Policy and ActionPass:",
7905
+ " policy · test-policy · revoke · revocations · issuer-keys · status · caep-map",
7906
+ "",
7907
+ "Agent runtimes and connectors:",
7908
+ " hook · mcp · connect · connections · disconnect · run",
7909
+ "",
7910
+ "Evidence and verification:",
7911
+ " export-ledger · sync-ledger · export-otel · attest-ledger · verify-export",
7912
+ " prove-equivalence · prove-inclusion · prove-consistency · verify-proof · forensics · acs",
7913
+ "",
7914
+ "Run `axtary <command> --help` for detailed usage. Nested groups work too,",
7915
+ "for example `axtary mcp conformance --help`.",
7916
+ "",
7917
+ "Global options: --help, --version, --no-color",
7918
+ "",
7919
+ ].join("\n");
7920
+ }
7921
+ function commandHelpText(command, args) {
7922
+ const helpIndex = args.findIndex((arg) => arg === "--help" || arg === "-h");
7923
+ if (helpIndex < 0)
7924
+ return null;
7925
+ const nested = args.slice(0, helpIndex).filter((arg) => !arg.startsWith("-"));
7926
+ const prefix = [command, ...nested].join(" ");
7927
+ const matching = DETAILED_USAGE_LINES.filter((line) => {
7928
+ const usage = line.trimStart();
7929
+ return usage === `axtary ${prefix}` || usage.startsWith(`axtary ${prefix} `);
7930
+ });
7931
+ if (matching.length === 0)
7932
+ return null;
7933
+ return [
7934
+ `Usage for \`${prefix}\`:`,
7935
+ ...matching,
7399
7936
  "",
7400
7937
  ].join("\n");
7401
7938
  }