@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/README.md +25 -10
- package/dist/index.d.ts +111 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +606 -69
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
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
|
|
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;
|
|
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({
|
|
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
|
|
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
|
-
? "
|
|
6585
|
-
: "connector is
|
|
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
|
-
"
|
|
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
|
|
6888
|
-
const
|
|
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:
|
|
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
|
|
7352
|
-
"
|
|
7353
|
-
"
|
|
7354
|
-
"
|
|
7355
|
-
"
|
|
7356
|
-
"
|
|
7357
|
-
"
|
|
7358
|
-
"
|
|
7359
|
-
"
|
|
7360
|
-
"
|
|
7361
|
-
"
|
|
7362
|
-
"
|
|
7363
|
-
"
|
|
7364
|
-
"
|
|
7365
|
-
"
|
|
7366
|
-
"
|
|
7367
|
-
"
|
|
7368
|
-
"
|
|
7369
|
-
"
|
|
7370
|
-
"
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
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
|
}
|