@axtary/cli 0.0.1 → 0.1.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/LICENSE +202 -0
- package/README.md +27 -3
- package/dist/claude-code-hook.d.ts +45 -0
- package/dist/claude-code-hook.d.ts.map +1 -0
- package/dist/claude-code-hook.js +246 -0
- package/dist/claude-code-hook.js.map +1 -0
- package/dist/index.d.ts +133 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1166 -11
- package/dist/index.js.map +1 -1
- package/package.json +11 -12
package/dist/index.js
CHANGED
|
@@ -2,18 +2,43 @@ import { generateKeyPairSync } from "node:crypto";
|
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { extname, resolve } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { AxtaryDecisionSchema, createApprovalArtifact, demoAction, hashPayload, parseNormalizedAction, } from "@axtary/actionpass";
|
|
6
7
|
import { createFakeAdapterState, createFakeHandlers, createAwsRestHandlers, createGcpRestHandlers, createGitHubRestHandlers, createLinearGraphqlHandlers, createLocalDocsHandlers, createSlackWebHandlers, smokeAwsRestCredentials, smokeGcpRestCredentials, smokeGitHubRestCredentials, smokeLinearGraphqlCredentials, smokeLocalDocsRoots, smokeSlackWebCredentials, } from "@axtary/adapters";
|
|
7
8
|
import { loadAxtaryConfig, CachedConfigLoader } from "@axtary/config";
|
|
8
9
|
import { exportLedgerRecords, formatLedgerExport, LedgerExportFormatSchema, LocalJsonlLedger, syncLedgerExport, } from "@axtary/ledger";
|
|
9
|
-
import { MCP_TOOL_CALL, createMcpAction, createMcpFixtureToolHandler, createMcpToolDefinition, } from "@axtary/mcp";
|
|
10
|
-
import { evaluatePolicy } from "@axtary/policy";
|
|
10
|
+
import { MCP_TOOL_CALL, McpStdioClient, createMcpAction, createMcpFixtureToolHandler, createMcpToolDefinition, createMcpToolRouter, createMcpWrapperServer, createMcpWrapperTool, } from "@axtary/mcp";
|
|
11
|
+
import { evaluatePolicy, explainPolicyDecision } from "@axtary/policy";
|
|
11
12
|
import { createProxyRuntime, } from "@axtary/proxy";
|
|
13
|
+
import { runClaudeCodeHook } from "./claude-code-hook.js";
|
|
14
|
+
export { CLAUDE_CODE_HOOK_SCHEMA_VERSION, normalizeClaudeCodeToolCall, parseClaudeCodeHookPayload, runClaudeCodeHook, } from "./claude-code-hook.js";
|
|
12
15
|
export const DEMO_SCHEMA_VERSION = "axtary.demo.v0";
|
|
13
16
|
export const POLICY_TEST_SCHEMA_VERSION = "axtary.policy_test.v0";
|
|
14
17
|
export const LOCAL_PROXY_SCHEMA_VERSION = "axtary.local_proxy.v0";
|
|
15
18
|
export const LEDGER_EXPORT_RUN_SCHEMA_VERSION = "axtary.ledger_export_run.v0";
|
|
16
19
|
export const LEDGER_SYNC_RUN_SCHEMA_VERSION = "axtary.ledger_sync_run.v0";
|
|
20
|
+
export const CONNECTOR_DOCTOR_SCHEMA_VERSION = "axtary.connector_doctor.v0";
|
|
21
|
+
export const REAL_WORKFLOW_RUN_SCHEMA_VERSION = "axtary.real_workflow_run.v0";
|
|
22
|
+
export function authorizeDecisionHints(decision, policy) {
|
|
23
|
+
const hints = [];
|
|
24
|
+
for (const reason of decision.reasons) {
|
|
25
|
+
if (reason === "slack_channel_not_allowed") {
|
|
26
|
+
hints.push(`Allowed Slack channels: ${policy.slack.messages.allowedChannels.join(", ") || "none configured"}.`);
|
|
27
|
+
}
|
|
28
|
+
if ((reason === "content_read_denied_path" ||
|
|
29
|
+
reason === "payload_touches_denied_path") &&
|
|
30
|
+
decision.constraints.blockedPaths.length > 0) {
|
|
31
|
+
hints.push(`Blocked path prefixes: ${decision.constraints.blockedPaths.join(", ")}.`);
|
|
32
|
+
}
|
|
33
|
+
if (reason === "payload_touches_step_up_path") {
|
|
34
|
+
hints.push("A human must approve the exact payload before this executes.");
|
|
35
|
+
}
|
|
36
|
+
if (reason.startsWith("unsupported_tool:")) {
|
|
37
|
+
hints.push("This tool is not governed by the active policy and fails closed.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return hints;
|
|
41
|
+
}
|
|
17
42
|
const MCP_DEMO_TOOL_DEFINITION = createMcpToolDefinition({
|
|
18
43
|
serverIdentity: "mcp:axtary-demo-fixtures",
|
|
19
44
|
serverVersion: "1.0.0",
|
|
@@ -250,6 +275,24 @@ export async function startProxyServer(input = {}) {
|
|
|
250
275
|
writeJson(response, statusCode, result);
|
|
251
276
|
return;
|
|
252
277
|
}
|
|
278
|
+
if (request.method === "POST" && request.url === "/authorize") {
|
|
279
|
+
const body = await readJsonBody(request);
|
|
280
|
+
const result = await runtime.authorize(body);
|
|
281
|
+
if (result.status === "malformed") {
|
|
282
|
+
writeJson(response, 400, result);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const policy = (await configLoader.getConfig()).config.policy;
|
|
286
|
+
writeJson(response, 200, {
|
|
287
|
+
...result,
|
|
288
|
+
actionPass: result.actionPass
|
|
289
|
+
? { token: result.actionPass.token, claims: result.actionPass.claims }
|
|
290
|
+
: null,
|
|
291
|
+
explanation: explainPolicyDecision(result.decision),
|
|
292
|
+
hints: authorizeDecisionHints(result.decision, policy),
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
253
296
|
writeJson(response, 404, {
|
|
254
297
|
schemaVersion: LOCAL_PROXY_SCHEMA_VERSION,
|
|
255
298
|
error: "not_found",
|
|
@@ -276,6 +319,245 @@ export async function startProxyServer(input = {}) {
|
|
|
276
319
|
close: () => close(server),
|
|
277
320
|
};
|
|
278
321
|
}
|
|
322
|
+
export const INIT_SCHEMA_VERSION = "axtary.init.v0";
|
|
323
|
+
export const INIT_CONFIG_TEMPLATE = `schemaVersion: axtary.config.v0
|
|
324
|
+
issuer: https://local.axtary.dev
|
|
325
|
+
tenant: org:local
|
|
326
|
+
ledger:
|
|
327
|
+
path: .axtary/actions.jsonl
|
|
328
|
+
runtime:
|
|
329
|
+
allowUnsignedExecution: false
|
|
330
|
+
handlerTimeoutMs: 10000
|
|
331
|
+
adapters:
|
|
332
|
+
# Start in fake mode: deterministic local adapters, no credentials needed.
|
|
333
|
+
# Switch a provider to its real mode (rest/web/graphql) once its env token is set.
|
|
334
|
+
github:
|
|
335
|
+
mode: fake
|
|
336
|
+
tokenEnv: GITHUB_TOKEN
|
|
337
|
+
apiBaseUrl: https://api.github.com
|
|
338
|
+
userAgent: axtary-local-proxy
|
|
339
|
+
slack:
|
|
340
|
+
mode: fake
|
|
341
|
+
tokenEnv: SLACK_BOT_TOKEN
|
|
342
|
+
apiBaseUrl: https://slack.com/api
|
|
343
|
+
linear:
|
|
344
|
+
mode: fake
|
|
345
|
+
tokenEnv: LINEAR_API_KEY
|
|
346
|
+
apiUrl: https://api.linear.app/graphql
|
|
347
|
+
policy:
|
|
348
|
+
version: "${new Date().toISOString().slice(0, 10)}"
|
|
349
|
+
github:
|
|
350
|
+
pullRequests:
|
|
351
|
+
requiredBaseBranch: main
|
|
352
|
+
maxFilesChanged: 12
|
|
353
|
+
denyPathPrefixes:
|
|
354
|
+
- .env
|
|
355
|
+
- secrets/
|
|
356
|
+
stepUpPathPrefixes:
|
|
357
|
+
- infra/prod/
|
|
358
|
+
- billing/
|
|
359
|
+
- auth/
|
|
360
|
+
requiresTests: true
|
|
361
|
+
contents:
|
|
362
|
+
denyReadPathPrefixes:
|
|
363
|
+
- .env
|
|
364
|
+
- secrets/
|
|
365
|
+
- infra/prod/
|
|
366
|
+
slack:
|
|
367
|
+
messages:
|
|
368
|
+
allowedChannels:
|
|
369
|
+
- "#axtary-dev"
|
|
370
|
+
stepUpExternalRecipients: true
|
|
371
|
+
demo:
|
|
372
|
+
scenario: coding-agent
|
|
373
|
+
`;
|
|
374
|
+
export async function runInit(input = {}) {
|
|
375
|
+
const cwd = resolve(input.cwd ?? process.cwd());
|
|
376
|
+
const configPath = resolve(cwd, input.configPath ?? "axtary.yml");
|
|
377
|
+
const exists = await stat(configPath).then(() => true, () => false);
|
|
378
|
+
if (exists && input.force !== true) {
|
|
379
|
+
return {
|
|
380
|
+
schemaVersion: INIT_SCHEMA_VERSION,
|
|
381
|
+
status: "exists",
|
|
382
|
+
configPath,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
await writeFile(configPath, INIT_CONFIG_TEMPLATE, "utf8");
|
|
386
|
+
return {
|
|
387
|
+
schemaVersion: INIT_SCHEMA_VERSION,
|
|
388
|
+
status: "created",
|
|
389
|
+
configPath,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
export function formatInitResult(result) {
|
|
393
|
+
if (result.status === "exists") {
|
|
394
|
+
return [
|
|
395
|
+
`axtary.yml already exists at ${result.configPath}.`,
|
|
396
|
+
"Re-run with --force to overwrite it.",
|
|
397
|
+
"",
|
|
398
|
+
].join("\n");
|
|
399
|
+
}
|
|
400
|
+
return [
|
|
401
|
+
`Created ${result.configPath}.`,
|
|
402
|
+
"",
|
|
403
|
+
"Next steps (the ten-minute rail, see docs/quickstart.md):",
|
|
404
|
+
" 1. axtary doctor connectors --config axtary.yml # readiness checklist; fake mode needs no credentials",
|
|
405
|
+
" 2. axtary proxy --config axtary.yml # start the local enforcement point",
|
|
406
|
+
" 3. axtary demo --config axtary.yml # watch safe + unsafe actions hit policy and the ledger",
|
|
407
|
+
" 4. Gate a live Claude Code session: docs/claude-code-hook.md (PreToolUse hook)",
|
|
408
|
+
" or any MCP agent: docs/mcp-wrapper.md (axtary mcp serve)",
|
|
409
|
+
"",
|
|
410
|
+
"First blocked action without an agent:",
|
|
411
|
+
" with the proxy running, ask it to read .env.production through the hook:",
|
|
412
|
+
" echo '{\"tool_name\":\"Read\",\"tool_input\":{\"file_path\":\".env.production\"}}' | axtary hook claude-code",
|
|
413
|
+
"",
|
|
414
|
+
].join("\n");
|
|
415
|
+
}
|
|
416
|
+
export const MCP_SERVE_SCHEMA_VERSION = "axtary.mcp_serve.v0";
|
|
417
|
+
export async function startMcpServe(input = {}) {
|
|
418
|
+
const loadedConfig = await loadAxtaryConfig({
|
|
419
|
+
filePath: input.configPath,
|
|
420
|
+
cwd: input.cwd,
|
|
421
|
+
});
|
|
422
|
+
const cwd = resolve(input.cwd ?? process.cwd());
|
|
423
|
+
const ledgerPath = input.ledgerPath
|
|
424
|
+
? resolve(cwd, input.ledgerPath)
|
|
425
|
+
: loadedConfig.ledgerPath;
|
|
426
|
+
const ledger = new LocalJsonlLedger(ledgerPath);
|
|
427
|
+
const { privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
428
|
+
const log = input.log ?? ((text) => process.stderr.write(text));
|
|
429
|
+
let upstream = null;
|
|
430
|
+
let tools;
|
|
431
|
+
if (input.wrap) {
|
|
432
|
+
const [command, ...commandArgs] = input.wrap.split(/\s+/).filter(Boolean);
|
|
433
|
+
if (!command) {
|
|
434
|
+
throw new Error("mcp_serve_wrap_command_required");
|
|
435
|
+
}
|
|
436
|
+
upstream = await McpStdioClient.start({ command, args: commandArgs });
|
|
437
|
+
const serverInfo = upstream.serverInfo;
|
|
438
|
+
const listings = await upstream.listTools();
|
|
439
|
+
const client = upstream;
|
|
440
|
+
tools = listings.map((listing) => createMcpWrapperTool({
|
|
441
|
+
definition: {
|
|
442
|
+
serverIdentity: `mcp:${serverInfo.name}`,
|
|
443
|
+
serverVersion: serverInfo.version,
|
|
444
|
+
schemaVersion: serverInfo.version,
|
|
445
|
+
name: listing.name,
|
|
446
|
+
description: listing.description,
|
|
447
|
+
inputSchema: listing.inputSchema,
|
|
448
|
+
},
|
|
449
|
+
invoke: ({ call }) => client.callTool(call.toolName, call.arguments),
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
tools = [
|
|
454
|
+
createMcpWrapperTool({
|
|
455
|
+
definition: MCP_DEMO_TOOL_DEFINITION,
|
|
456
|
+
invoke: ({ call }) => {
|
|
457
|
+
const fixtureKey = typeof call.arguments.fixtureKey === "string"
|
|
458
|
+
? call.arguments.fixtureKey
|
|
459
|
+
: "";
|
|
460
|
+
const fixture = MCP_DEMO_FIXTURES[fixtureKey];
|
|
461
|
+
if (fixture === undefined) {
|
|
462
|
+
throw new Error(`mcp_fixture_not_found:${fixtureKey}`);
|
|
463
|
+
}
|
|
464
|
+
return fixture;
|
|
465
|
+
},
|
|
466
|
+
}),
|
|
467
|
+
];
|
|
468
|
+
}
|
|
469
|
+
if (tools.length === 0) {
|
|
470
|
+
upstream?.close();
|
|
471
|
+
throw new Error("mcp_serve_no_tools_discovered");
|
|
472
|
+
}
|
|
473
|
+
const policy = {
|
|
474
|
+
...loadedConfig.config.policy,
|
|
475
|
+
mcp: {
|
|
476
|
+
...loadedConfig.config.policy.mcp,
|
|
477
|
+
enabled: true,
|
|
478
|
+
allowedTools: [
|
|
479
|
+
...(loadedConfig.config.policy.mcp.allowedTools ?? []),
|
|
480
|
+
...tools.map((tool) => ({
|
|
481
|
+
serverIdentity: tool.definition.serverIdentity,
|
|
482
|
+
toolName: tool.definition.name,
|
|
483
|
+
definitionHash: tool.definition.definitionHash,
|
|
484
|
+
})),
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
const runtime = createProxyRuntime({
|
|
489
|
+
issuer: loadedConfig.config.issuer,
|
|
490
|
+
tenant: loadedConfig.config.tenant,
|
|
491
|
+
ledger,
|
|
492
|
+
policy,
|
|
493
|
+
signingKey: privateKey,
|
|
494
|
+
keyId: "local-mcp-serve-key",
|
|
495
|
+
allowUnsignedExecution: loadedConfig.config.runtime.allowUnsignedExecution,
|
|
496
|
+
handlerTimeoutMs: loadedConfig.config.runtime.handlerTimeoutMs,
|
|
497
|
+
handlers: {
|
|
498
|
+
[MCP_TOOL_CALL]: createMcpToolRouter(tools),
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
const server = createMcpWrapperServer({
|
|
502
|
+
tools,
|
|
503
|
+
runtime,
|
|
504
|
+
actor: {
|
|
505
|
+
agentId: "agent:mcp-client",
|
|
506
|
+
humanOwner: input.owner ?? "user:local-developer",
|
|
507
|
+
runtime: "mcp",
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
const inputStream = input.input ?? process.stdin;
|
|
511
|
+
const outputStream = input.output ?? process.stdout;
|
|
512
|
+
const readline = createInterface({ input: inputStream });
|
|
513
|
+
let pendingWrites = Promise.resolve();
|
|
514
|
+
readline.on("line", (line) => {
|
|
515
|
+
const trimmed = line.trim();
|
|
516
|
+
if (trimmed.length === 0) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
pendingWrites = pendingWrites.then(async () => {
|
|
520
|
+
let response;
|
|
521
|
+
try {
|
|
522
|
+
response = await server.handleMessage(JSON.parse(trimmed));
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
response = {
|
|
526
|
+
jsonrpc: "2.0",
|
|
527
|
+
id: null,
|
|
528
|
+
error: { code: -32700, message: "parse_error" },
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (response !== null) {
|
|
532
|
+
outputStream.write(`${JSON.stringify(response)}\n`);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
const done = new Promise((resolveDone) => {
|
|
537
|
+
readline.once("close", () => {
|
|
538
|
+
void pendingWrites.finally(resolveDone);
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
for (const tool of tools) {
|
|
542
|
+
log(`wrapped ${tool.definition.serverIdentity}/${tool.definition.name} ${tool.definition.definitionHash}\n`);
|
|
543
|
+
}
|
|
544
|
+
log(`ledger: ${ledgerPath}\n`);
|
|
545
|
+
return {
|
|
546
|
+
schemaVersion: MCP_SERVE_SCHEMA_VERSION,
|
|
547
|
+
ledgerPath,
|
|
548
|
+
tools: tools.map((tool) => ({
|
|
549
|
+
name: tool.definition.name,
|
|
550
|
+
serverIdentity: tool.definition.serverIdentity,
|
|
551
|
+
definitionHash: tool.definition.definitionHash,
|
|
552
|
+
})),
|
|
553
|
+
done,
|
|
554
|
+
close: async () => {
|
|
555
|
+
readline.close();
|
|
556
|
+
upstream?.close();
|
|
557
|
+
await pendingWrites;
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
279
561
|
export async function runDemo(input = {}) {
|
|
280
562
|
const loadedConfig = await loadAxtaryConfig({
|
|
281
563
|
filePath: input.configPath,
|
|
@@ -533,6 +815,157 @@ export async function runSmokeTest(input = {}, io = {}) {
|
|
|
533
815
|
}
|
|
534
816
|
return failed ? 1 : 0;
|
|
535
817
|
}
|
|
818
|
+
export async function runConnectorDoctor(input = {}) {
|
|
819
|
+
const env = input.env ?? process.env;
|
|
820
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
821
|
+
const loadedConfig = await loadAxtaryConfig({
|
|
822
|
+
filePath: input.configPath,
|
|
823
|
+
cwd: input.cwd,
|
|
824
|
+
});
|
|
825
|
+
const providers = [];
|
|
826
|
+
const githubConfig = loadedConfig.config.adapters.github;
|
|
827
|
+
providers.push(await doctorProvider({
|
|
828
|
+
provider: "github",
|
|
829
|
+
mode: githubConfig.mode,
|
|
830
|
+
activeMode: "rest",
|
|
831
|
+
capability: githubConfig.mode === "rest" ? "real-write" : "fake",
|
|
832
|
+
requiredEnv: githubConfig.mode === "rest" ? [githubConfig.tokenEnv] : [],
|
|
833
|
+
requiredScopes: githubConfig.mode === "rest"
|
|
834
|
+
? ["pull_requests:write", "contents:write", "metadata:read"]
|
|
835
|
+
: [],
|
|
836
|
+
smokeCheck: githubConfig.mode === "rest" ? "GET /user" : null,
|
|
837
|
+
env,
|
|
838
|
+
runSmoke: githubConfig.mode === "rest"
|
|
839
|
+
? () => smokeGitHubRestCredentials({
|
|
840
|
+
token: env[githubConfig.tokenEnv] ?? "",
|
|
841
|
+
apiBaseUrl: githubConfig.apiBaseUrl,
|
|
842
|
+
userAgent: githubConfig.userAgent,
|
|
843
|
+
fetch: fetchImpl,
|
|
844
|
+
maxRetries: 0,
|
|
845
|
+
})
|
|
846
|
+
: undefined,
|
|
847
|
+
}));
|
|
848
|
+
const slackConfig = loadedConfig.config.adapters.slack;
|
|
849
|
+
providers.push(await doctorProvider({
|
|
850
|
+
provider: "slack",
|
|
851
|
+
mode: slackConfig.mode,
|
|
852
|
+
activeMode: "web",
|
|
853
|
+
capability: slackConfig.mode === "web" ? "real-write" : "fake",
|
|
854
|
+
requiredEnv: slackConfig.mode === "web" ? [slackConfig.tokenEnv] : [],
|
|
855
|
+
requiredScopes: slackConfig.mode === "web" ? ["chat:write"] : [],
|
|
856
|
+
smokeCheck: slackConfig.mode === "web" ? "auth.test" : null,
|
|
857
|
+
env,
|
|
858
|
+
runSmoke: slackConfig.mode === "web"
|
|
859
|
+
? () => smokeSlackWebCredentials({
|
|
860
|
+
token: env[slackConfig.tokenEnv] ?? "",
|
|
861
|
+
apiBaseUrl: slackConfig.apiBaseUrl,
|
|
862
|
+
fetch: fetchImpl,
|
|
863
|
+
maxRetries: 0,
|
|
864
|
+
})
|
|
865
|
+
: undefined,
|
|
866
|
+
}));
|
|
867
|
+
const linearConfig = loadedConfig.config.adapters.linear;
|
|
868
|
+
providers.push(await doctorProvider({
|
|
869
|
+
provider: "linear",
|
|
870
|
+
mode: linearConfig.mode,
|
|
871
|
+
activeMode: "graphql",
|
|
872
|
+
capability: linearConfig.mode === "graphql" ? "real-write" : "fake",
|
|
873
|
+
requiredEnv: linearConfig.mode === "graphql" ? [linearConfig.tokenEnv] : [],
|
|
874
|
+
requiredScopes: linearConfig.mode === "graphql" ? ["issues:read", "issues:write"] : [],
|
|
875
|
+
smokeCheck: linearConfig.mode === "graphql" ? "viewer" : null,
|
|
876
|
+
env,
|
|
877
|
+
runSmoke: linearConfig.mode === "graphql"
|
|
878
|
+
? () => smokeLinearGraphqlCredentials({
|
|
879
|
+
token: env[linearConfig.tokenEnv] ?? "",
|
|
880
|
+
apiUrl: linearConfig.apiUrl,
|
|
881
|
+
fetch: fetchImpl,
|
|
882
|
+
maxRetries: 0,
|
|
883
|
+
})
|
|
884
|
+
: undefined,
|
|
885
|
+
}));
|
|
886
|
+
const awsConfig = loadedConfig.config.adapters.aws;
|
|
887
|
+
providers.push(await doctorProvider({
|
|
888
|
+
provider: "aws",
|
|
889
|
+
mode: awsConfig.mode,
|
|
890
|
+
activeMode: "rest",
|
|
891
|
+
capability: awsConfig.mode === "rest" ? "read-only" : "off",
|
|
892
|
+
requiredEnv: awsConfig.mode === "rest"
|
|
893
|
+
? [awsConfig.accessKeyIdEnv, awsConfig.secretAccessKeyEnv]
|
|
894
|
+
: [],
|
|
895
|
+
requiredScopes: awsConfig.mode === "rest" ? ["sts:GetCallerIdentity"] : [],
|
|
896
|
+
smokeCheck: awsConfig.mode === "rest" ? "STS GetCallerIdentity" : null,
|
|
897
|
+
env,
|
|
898
|
+
runSmoke: awsConfig.mode === "rest"
|
|
899
|
+
? () => smokeAwsRestCredentials({
|
|
900
|
+
credentials: {
|
|
901
|
+
accessKeyId: env[awsConfig.accessKeyIdEnv] ?? "",
|
|
902
|
+
secretAccessKey: env[awsConfig.secretAccessKeyEnv] ?? "",
|
|
903
|
+
sessionToken: env[awsConfig.sessionTokenEnv],
|
|
904
|
+
},
|
|
905
|
+
region: awsConfig.region,
|
|
906
|
+
stsUrl: awsConfig.stsUrl,
|
|
907
|
+
fetch: fetchImpl,
|
|
908
|
+
maxRetries: 0,
|
|
909
|
+
})
|
|
910
|
+
: undefined,
|
|
911
|
+
}));
|
|
912
|
+
const gcpConfig = loadedConfig.config.adapters.gcp;
|
|
913
|
+
providers.push(await doctorProvider({
|
|
914
|
+
provider: "gcp",
|
|
915
|
+
mode: gcpConfig.mode,
|
|
916
|
+
activeMode: "rest",
|
|
917
|
+
capability: gcpConfig.mode === "rest" ? "read-only" : "off",
|
|
918
|
+
requiredEnv: gcpConfig.mode === "rest" ? [gcpConfig.accessTokenEnv] : [],
|
|
919
|
+
requiredScopes: gcpConfig.mode === "rest"
|
|
920
|
+
? ["cloudresourcemanager.projects.get", "storage.objects.list"]
|
|
921
|
+
: [],
|
|
922
|
+
smokeCheck: gcpConfig.mode === "rest"
|
|
923
|
+
? "Cloud Resource Manager project get/list"
|
|
924
|
+
: null,
|
|
925
|
+
env,
|
|
926
|
+
runSmoke: gcpConfig.mode === "rest"
|
|
927
|
+
? () => smokeGcpRestCredentials({
|
|
928
|
+
accessToken: env[gcpConfig.accessTokenEnv] ?? "",
|
|
929
|
+
projectId: gcpConfig.projectId,
|
|
930
|
+
cloudResourceManagerApiBaseUrl: gcpConfig.cloudResourceManagerApiBaseUrl,
|
|
931
|
+
storageApiBaseUrl: gcpConfig.storageApiBaseUrl,
|
|
932
|
+
fetch: fetchImpl,
|
|
933
|
+
maxRetries: 0,
|
|
934
|
+
})
|
|
935
|
+
: undefined,
|
|
936
|
+
}));
|
|
937
|
+
const docsConfig = loadedConfig.config.adapters.docs;
|
|
938
|
+
providers.push(await doctorProvider({
|
|
939
|
+
provider: "docs",
|
|
940
|
+
mode: docsConfig.mode,
|
|
941
|
+
activeMode: "local",
|
|
942
|
+
capability: docsConfig.mode === "local" ? "local-read" : "off",
|
|
943
|
+
requiredEnv: [],
|
|
944
|
+
requiredScopes: docsConfig.mode === "local" ? ["configured_roots:read"] : [],
|
|
945
|
+
smokeCheck: docsConfig.mode === "local" ? "configured roots exist" : null,
|
|
946
|
+
env,
|
|
947
|
+
runSmoke: docsConfig.mode === "local"
|
|
948
|
+
? () => smokeLocalDocsRoots({
|
|
949
|
+
workspace: docsConfig.workspace,
|
|
950
|
+
roots: resolveDocsRoots(loadedConfig),
|
|
951
|
+
maxSearchResults: docsConfig.maxSearchResults,
|
|
952
|
+
maxReadBytes: docsConfig.maxReadBytes,
|
|
953
|
+
allowedExtensions: docsConfig.allowedExtensions,
|
|
954
|
+
})
|
|
955
|
+
: undefined,
|
|
956
|
+
}));
|
|
957
|
+
return {
|
|
958
|
+
schemaVersion: CONNECTOR_DOCTOR_SCHEMA_VERSION,
|
|
959
|
+
config: {
|
|
960
|
+
filePath: loadedConfig.filePath,
|
|
961
|
+
tenant: loadedConfig.config.tenant,
|
|
962
|
+
},
|
|
963
|
+
status: providers.some((provider) => provider.status === "missing" || provider.status === "failed")
|
|
964
|
+
? "needs_setup"
|
|
965
|
+
: "ready",
|
|
966
|
+
providers,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
536
969
|
export async function runTestPolicy(input) {
|
|
537
970
|
const loadedConfig = await loadAxtaryConfig({
|
|
538
971
|
filePath: input.configPath,
|
|
@@ -643,6 +1076,105 @@ export async function runLedgerSync(input) {
|
|
|
643
1076
|
sync,
|
|
644
1077
|
};
|
|
645
1078
|
}
|
|
1079
|
+
export async function runRealWorkflow(input = {}) {
|
|
1080
|
+
const loadedConfig = await loadAxtaryConfig({
|
|
1081
|
+
filePath: input.configPath,
|
|
1082
|
+
cwd: input.cwd,
|
|
1083
|
+
});
|
|
1084
|
+
assertRealWorkflowModes(loadedConfig);
|
|
1085
|
+
const env = input.env ?? process.env;
|
|
1086
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
1087
|
+
const ledgerPath = resolveConfiguredLedgerPath(input, loadedConfig);
|
|
1088
|
+
const ledger = new LocalJsonlLedger(ledgerPath);
|
|
1089
|
+
const adapterState = createFakeAdapterState();
|
|
1090
|
+
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
1091
|
+
const { handlers } = createProxyHandlers(loadedConfig, adapterState, {
|
|
1092
|
+
issuer: loadedConfig.config.issuer,
|
|
1093
|
+
verificationKey: publicKey,
|
|
1094
|
+
}, {
|
|
1095
|
+
env,
|
|
1096
|
+
fetch: fetchImpl,
|
|
1097
|
+
});
|
|
1098
|
+
const approvedBy = input.approvedBy ?? "user:local-reviewer";
|
|
1099
|
+
const workflowInputs = workflowInputsFrom(input, loadedConfig);
|
|
1100
|
+
const actions = realWorkflowActions(workflowInputs);
|
|
1101
|
+
const tamperSetup = input.tamper === true ? prepareTamperedSlackStep(actions, approvedBy) : null;
|
|
1102
|
+
const runtime = createProxyRuntime({
|
|
1103
|
+
issuer: loadedConfig.config.issuer,
|
|
1104
|
+
tenant: loadedConfig.config.tenant,
|
|
1105
|
+
ledger,
|
|
1106
|
+
policy: loadedConfig.config.policy,
|
|
1107
|
+
signingKey: privateKey,
|
|
1108
|
+
keyId: "real-workflow-local-key",
|
|
1109
|
+
allowUnsignedExecution: loadedConfig.config.runtime.allowUnsignedExecution,
|
|
1110
|
+
handlerTimeoutMs: loadedConfig.config.runtime.handlerTimeoutMs,
|
|
1111
|
+
handlers,
|
|
1112
|
+
approvalResolver: input.approveStepUp || tamperSetup
|
|
1113
|
+
? ({ action }) => {
|
|
1114
|
+
if (tamperSetup && action.capability.tool === tamperSetup.tool) {
|
|
1115
|
+
return tamperSetup.staleArtifact;
|
|
1116
|
+
}
|
|
1117
|
+
if (!input.approveStepUp) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
return createApprovalArtifact({
|
|
1121
|
+
action,
|
|
1122
|
+
mode: "human",
|
|
1123
|
+
approvedBy,
|
|
1124
|
+
reason: "Local exact approval for real workflow step-up action.",
|
|
1125
|
+
}).artifact;
|
|
1126
|
+
}
|
|
1127
|
+
: undefined,
|
|
1128
|
+
});
|
|
1129
|
+
const steps = [];
|
|
1130
|
+
for (const action of actions) {
|
|
1131
|
+
const result = await runtime.handle(action.action);
|
|
1132
|
+
steps.push(toRealWorkflowStepResult(action.name, result));
|
|
1133
|
+
if (result.status !== "executed") {
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const verifiedLedger = await ledger.verify();
|
|
1138
|
+
const status = steps.some((step) => step.status === "failed")
|
|
1139
|
+
? "failed"
|
|
1140
|
+
: steps.length < actions.length ||
|
|
1141
|
+
steps.some((step) => step.status === "blocked" || step.status === "malformed")
|
|
1142
|
+
? "blocked"
|
|
1143
|
+
: "completed";
|
|
1144
|
+
const tamperStep = tamperSetup
|
|
1145
|
+
? steps.find((step) => step.name === TAMPER_STEP_NAME) ?? null
|
|
1146
|
+
: null;
|
|
1147
|
+
return {
|
|
1148
|
+
schemaVersion: REAL_WORKFLOW_RUN_SCHEMA_VERSION,
|
|
1149
|
+
workflow: "github-pr-review",
|
|
1150
|
+
config: {
|
|
1151
|
+
filePath: loadedConfig.filePath,
|
|
1152
|
+
tenant: loadedConfig.config.tenant,
|
|
1153
|
+
},
|
|
1154
|
+
ledgerPath,
|
|
1155
|
+
status,
|
|
1156
|
+
requiredModes: {
|
|
1157
|
+
github: "rest",
|
|
1158
|
+
slack: "web",
|
|
1159
|
+
linear: "graphql",
|
|
1160
|
+
docs: "local",
|
|
1161
|
+
},
|
|
1162
|
+
inputs: workflowInputs,
|
|
1163
|
+
steps,
|
|
1164
|
+
tamper: tamperSetup
|
|
1165
|
+
? {
|
|
1166
|
+
step: TAMPER_STEP_NAME,
|
|
1167
|
+
tool: tamperSetup.tool,
|
|
1168
|
+
mutation: tamperSetup.mutation,
|
|
1169
|
+
approvedPayloadHash: tamperSetup.approvedPayloadHash,
|
|
1170
|
+
attemptedPayloadHash: tamperSetup.attemptedPayloadHash,
|
|
1171
|
+
blocked: tamperStep?.status === "blocked",
|
|
1172
|
+
blockReasons: tamperStep?.reasons ?? [],
|
|
1173
|
+
}
|
|
1174
|
+
: null,
|
|
1175
|
+
ledger: ledgerSummary(verifiedLedger),
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
646
1178
|
export async function runAxtaryCli(argv = process.argv.slice(2), io = {}) {
|
|
647
1179
|
const stdout = io.stdout ?? ((text) => process.stdout.write(text));
|
|
648
1180
|
const stderr = io.stderr ?? ((text) => process.stderr.write(text));
|
|
@@ -652,6 +1184,21 @@ export async function runAxtaryCli(argv = process.argv.slice(2), io = {}) {
|
|
|
652
1184
|
stdout(helpText());
|
|
653
1185
|
return 0;
|
|
654
1186
|
}
|
|
1187
|
+
if (command === "init") {
|
|
1188
|
+
const parsed = parseInitArgs(args);
|
|
1189
|
+
const result = await runInit({
|
|
1190
|
+
configPath: parsed.configPath,
|
|
1191
|
+
cwd: io.cwd ?? parsed.cwd,
|
|
1192
|
+
force: parsed.force,
|
|
1193
|
+
});
|
|
1194
|
+
if (parsed.json) {
|
|
1195
|
+
stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
stdout(formatInitResult(result));
|
|
1199
|
+
}
|
|
1200
|
+
return result.status === "created" ? 0 : 1;
|
|
1201
|
+
}
|
|
655
1202
|
if (command === "smoke") {
|
|
656
1203
|
const parsed = parseSmokeArgs(args);
|
|
657
1204
|
return await runSmokeTest({
|
|
@@ -659,6 +1206,20 @@ export async function runAxtaryCli(argv = process.argv.slice(2), io = {}) {
|
|
|
659
1206
|
cwd: io.cwd ?? parsed.cwd,
|
|
660
1207
|
}, io);
|
|
661
1208
|
}
|
|
1209
|
+
if (command === "doctor") {
|
|
1210
|
+
const parsed = parseDoctorArgs(args);
|
|
1211
|
+
const result = await runConnectorDoctor({
|
|
1212
|
+
configPath: parsed.configPath,
|
|
1213
|
+
cwd: io.cwd ?? parsed.cwd,
|
|
1214
|
+
});
|
|
1215
|
+
if (parsed.json) {
|
|
1216
|
+
stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
1217
|
+
}
|
|
1218
|
+
else {
|
|
1219
|
+
stdout(formatConnectorDoctorResult(result));
|
|
1220
|
+
}
|
|
1221
|
+
return result.status === "ready" ? 0 : 1;
|
|
1222
|
+
}
|
|
662
1223
|
if (command === "demo") {
|
|
663
1224
|
const parsed = parseDemoArgs(args);
|
|
664
1225
|
const result = await runDemo({
|
|
@@ -715,6 +1276,69 @@ export async function runAxtaryCli(argv = process.argv.slice(2), io = {}) {
|
|
|
715
1276
|
}
|
|
716
1277
|
return 0;
|
|
717
1278
|
}
|
|
1279
|
+
if (command === "run") {
|
|
1280
|
+
const parsed = parseRunWorkflowArgs(args);
|
|
1281
|
+
const result = await runRealWorkflow({
|
|
1282
|
+
configPath: parsed.configPath,
|
|
1283
|
+
ledgerPath: parsed.ledgerPath,
|
|
1284
|
+
cwd: io.cwd ?? parsed.cwd,
|
|
1285
|
+
repoResource: parsed.repoResource,
|
|
1286
|
+
linearIssueKey: parsed.linearIssueKey,
|
|
1287
|
+
projectKey: parsed.projectKey,
|
|
1288
|
+
docsQuery: parsed.docsQuery,
|
|
1289
|
+
slackChannel: parsed.slackChannel,
|
|
1290
|
+
baseBranch: parsed.baseBranch,
|
|
1291
|
+
headBranch: parsed.headBranch,
|
|
1292
|
+
prTitle: parsed.prTitle,
|
|
1293
|
+
approvedBy: parsed.approvedBy,
|
|
1294
|
+
approveStepUp: parsed.approveStepUp,
|
|
1295
|
+
tamper: parsed.tamper,
|
|
1296
|
+
});
|
|
1297
|
+
if (parsed.json) {
|
|
1298
|
+
stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
1299
|
+
}
|
|
1300
|
+
else {
|
|
1301
|
+
stdout(formatRealWorkflowRunResult(result));
|
|
1302
|
+
}
|
|
1303
|
+
if (parsed.tamper) {
|
|
1304
|
+
return result.tamper?.blocked === true && result.ledger.valid ? 0 : 1;
|
|
1305
|
+
}
|
|
1306
|
+
return result.status === "completed" && result.ledger.valid ? 0 : 1;
|
|
1307
|
+
}
|
|
1308
|
+
if (command === "mcp") {
|
|
1309
|
+
const [sub, ...rest] = args;
|
|
1310
|
+
if (sub !== "serve") {
|
|
1311
|
+
throw new Error(`unknown_mcp_command:${sub ?? "missing"}`);
|
|
1312
|
+
}
|
|
1313
|
+
const parsed = parseMcpServeArgs(rest);
|
|
1314
|
+
const handle = await startMcpServe({
|
|
1315
|
+
configPath: parsed.configPath,
|
|
1316
|
+
ledgerPath: parsed.ledgerPath,
|
|
1317
|
+
cwd: io.cwd ?? parsed.cwd,
|
|
1318
|
+
wrap: parsed.wrap,
|
|
1319
|
+
owner: parsed.owner,
|
|
1320
|
+
log: stderr,
|
|
1321
|
+
});
|
|
1322
|
+
await handle.done;
|
|
1323
|
+
await handle.close();
|
|
1324
|
+
return 0;
|
|
1325
|
+
}
|
|
1326
|
+
if (command === "hook") {
|
|
1327
|
+
const parsed = parseHookArgs(args);
|
|
1328
|
+
const payloadText = await (io.stdin ?? readProcessStdin)();
|
|
1329
|
+
const result = await runClaudeCodeHook({
|
|
1330
|
+
payloadText,
|
|
1331
|
+
proxyUrl: parsed.proxyUrl,
|
|
1332
|
+
repoResource: parsed.repoResource,
|
|
1333
|
+
humanOwner: parsed.humanOwner,
|
|
1334
|
+
branch: parsed.branch,
|
|
1335
|
+
timeoutMs: parsed.timeoutMs,
|
|
1336
|
+
});
|
|
1337
|
+
if (result.output) {
|
|
1338
|
+
stdout(`${result.output}\n`);
|
|
1339
|
+
}
|
|
1340
|
+
return result.exitCode;
|
|
1341
|
+
}
|
|
718
1342
|
if (command === "proxy") {
|
|
719
1343
|
const parsed = parseProxyArgs(args);
|
|
720
1344
|
const server = await startProxyServer({
|
|
@@ -786,6 +1410,107 @@ function parseProxyArgs(args) {
|
|
|
786
1410
|
}
|
|
787
1411
|
return parsed;
|
|
788
1412
|
}
|
|
1413
|
+
function parseInitArgs(args) {
|
|
1414
|
+
const parsed = { force: false, json: false };
|
|
1415
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1416
|
+
const arg = args[index];
|
|
1417
|
+
switch (arg) {
|
|
1418
|
+
case "--config":
|
|
1419
|
+
parsed.configPath = requireValue(args, index, "--config");
|
|
1420
|
+
index += 1;
|
|
1421
|
+
break;
|
|
1422
|
+
case "--cwd":
|
|
1423
|
+
parsed.cwd = requireValue(args, index, "--cwd");
|
|
1424
|
+
index += 1;
|
|
1425
|
+
break;
|
|
1426
|
+
case "--force":
|
|
1427
|
+
parsed.force = true;
|
|
1428
|
+
break;
|
|
1429
|
+
case "--json":
|
|
1430
|
+
parsed.json = true;
|
|
1431
|
+
break;
|
|
1432
|
+
default:
|
|
1433
|
+
throw new Error(`unknown_init_flag:${arg}`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return parsed;
|
|
1437
|
+
}
|
|
1438
|
+
function parseMcpServeArgs(args) {
|
|
1439
|
+
const parsed = {};
|
|
1440
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1441
|
+
const arg = args[index];
|
|
1442
|
+
switch (arg) {
|
|
1443
|
+
case "--config":
|
|
1444
|
+
parsed.configPath = requireValue(args, index, "--config");
|
|
1445
|
+
index += 1;
|
|
1446
|
+
break;
|
|
1447
|
+
case "--ledger":
|
|
1448
|
+
parsed.ledgerPath = requireValue(args, index, "--ledger");
|
|
1449
|
+
index += 1;
|
|
1450
|
+
break;
|
|
1451
|
+
case "--cwd":
|
|
1452
|
+
parsed.cwd = requireValue(args, index, "--cwd");
|
|
1453
|
+
index += 1;
|
|
1454
|
+
break;
|
|
1455
|
+
case "--wrap":
|
|
1456
|
+
parsed.wrap = requireValue(args, index, "--wrap");
|
|
1457
|
+
index += 1;
|
|
1458
|
+
break;
|
|
1459
|
+
case "--owner":
|
|
1460
|
+
parsed.owner = requireValue(args, index, "--owner");
|
|
1461
|
+
index += 1;
|
|
1462
|
+
break;
|
|
1463
|
+
default:
|
|
1464
|
+
throw new Error(`unknown_mcp_serve_flag:${arg}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return parsed;
|
|
1468
|
+
}
|
|
1469
|
+
function parseHookArgs(args) {
|
|
1470
|
+
const [runtimeName, ...flags] = args;
|
|
1471
|
+
if (runtimeName !== "claude-code") {
|
|
1472
|
+
throw new Error(`unknown_hook_runtime:${runtimeName ?? "missing"}`);
|
|
1473
|
+
}
|
|
1474
|
+
const parsed = {};
|
|
1475
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
1476
|
+
const arg = flags[index];
|
|
1477
|
+
switch (arg) {
|
|
1478
|
+
case "--proxy":
|
|
1479
|
+
parsed.proxyUrl = requireValue(flags, index, "--proxy");
|
|
1480
|
+
index += 1;
|
|
1481
|
+
break;
|
|
1482
|
+
case "--repo":
|
|
1483
|
+
parsed.repoResource = normalizeRepoResource(requireValue(flags, index, "--repo"));
|
|
1484
|
+
index += 1;
|
|
1485
|
+
break;
|
|
1486
|
+
case "--owner":
|
|
1487
|
+
parsed.humanOwner = requireValue(flags, index, "--owner");
|
|
1488
|
+
index += 1;
|
|
1489
|
+
break;
|
|
1490
|
+
case "--branch":
|
|
1491
|
+
parsed.branch = requireValue(flags, index, "--branch");
|
|
1492
|
+
index += 1;
|
|
1493
|
+
break;
|
|
1494
|
+
case "--timeout":
|
|
1495
|
+
parsed.timeoutMs = Number.parseInt(requireValue(flags, index, "--timeout"), 10);
|
|
1496
|
+
if (!Number.isInteger(parsed.timeoutMs) || parsed.timeoutMs <= 0) {
|
|
1497
|
+
throw new Error("invalid_hook_timeout");
|
|
1498
|
+
}
|
|
1499
|
+
index += 1;
|
|
1500
|
+
break;
|
|
1501
|
+
default:
|
|
1502
|
+
throw new Error(`unknown_hook_flag:${arg}`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return parsed;
|
|
1506
|
+
}
|
|
1507
|
+
async function readProcessStdin() {
|
|
1508
|
+
const chunks = [];
|
|
1509
|
+
for await (const chunk of process.stdin) {
|
|
1510
|
+
chunks.push(Buffer.from(chunk));
|
|
1511
|
+
}
|
|
1512
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1513
|
+
}
|
|
789
1514
|
function parseTestPolicyArgs(args) {
|
|
790
1515
|
const parsed = {
|
|
791
1516
|
json: false,
|
|
@@ -836,6 +1561,115 @@ function parseSmokeArgs(args) {
|
|
|
836
1561
|
}
|
|
837
1562
|
return parsed;
|
|
838
1563
|
}
|
|
1564
|
+
function parseDoctorArgs(args) {
|
|
1565
|
+
const [subcommand, ...rest] = args;
|
|
1566
|
+
if (subcommand !== "connectors") {
|
|
1567
|
+
throw new Error("missing_doctor_subcommand:connectors");
|
|
1568
|
+
}
|
|
1569
|
+
const parsed = {
|
|
1570
|
+
json: false,
|
|
1571
|
+
};
|
|
1572
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
1573
|
+
const arg = rest[index];
|
|
1574
|
+
switch (arg) {
|
|
1575
|
+
case "--config":
|
|
1576
|
+
parsed.configPath = requireValue(rest, index, "--config");
|
|
1577
|
+
index += 1;
|
|
1578
|
+
break;
|
|
1579
|
+
case "--cwd":
|
|
1580
|
+
parsed.cwd = requireValue(rest, index, "--cwd");
|
|
1581
|
+
index += 1;
|
|
1582
|
+
break;
|
|
1583
|
+
case "--json":
|
|
1584
|
+
parsed.json = true;
|
|
1585
|
+
break;
|
|
1586
|
+
default:
|
|
1587
|
+
throw new Error(`unknown_doctor_flag:${arg}`);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return parsed;
|
|
1591
|
+
}
|
|
1592
|
+
function parseRunWorkflowArgs(args) {
|
|
1593
|
+
const [subcommand, workflow, ...rest] = args;
|
|
1594
|
+
if (subcommand !== "workflow" || workflow !== "github-pr-review") {
|
|
1595
|
+
throw new Error("missing_run_workflow:github-pr-review");
|
|
1596
|
+
}
|
|
1597
|
+
const parsed = {
|
|
1598
|
+
json: false,
|
|
1599
|
+
real: false,
|
|
1600
|
+
};
|
|
1601
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
1602
|
+
const arg = rest[index];
|
|
1603
|
+
switch (arg) {
|
|
1604
|
+
case "--real":
|
|
1605
|
+
parsed.real = true;
|
|
1606
|
+
break;
|
|
1607
|
+
case "--config":
|
|
1608
|
+
parsed.configPath = requireValue(rest, index, "--config");
|
|
1609
|
+
index += 1;
|
|
1610
|
+
break;
|
|
1611
|
+
case "--ledger":
|
|
1612
|
+
parsed.ledgerPath = requireValue(rest, index, "--ledger");
|
|
1613
|
+
index += 1;
|
|
1614
|
+
break;
|
|
1615
|
+
case "--cwd":
|
|
1616
|
+
parsed.cwd = requireValue(rest, index, "--cwd");
|
|
1617
|
+
index += 1;
|
|
1618
|
+
break;
|
|
1619
|
+
case "--repo":
|
|
1620
|
+
parsed.repoResource = normalizeRepoResource(requireValue(rest, index, "--repo"));
|
|
1621
|
+
index += 1;
|
|
1622
|
+
break;
|
|
1623
|
+
case "--linear-issue":
|
|
1624
|
+
parsed.linearIssueKey = requireValue(rest, index, "--linear-issue");
|
|
1625
|
+
index += 1;
|
|
1626
|
+
break;
|
|
1627
|
+
case "--project":
|
|
1628
|
+
parsed.projectKey = requireValue(rest, index, "--project");
|
|
1629
|
+
index += 1;
|
|
1630
|
+
break;
|
|
1631
|
+
case "--docs-query":
|
|
1632
|
+
parsed.docsQuery = requireValue(rest, index, "--docs-query");
|
|
1633
|
+
index += 1;
|
|
1634
|
+
break;
|
|
1635
|
+
case "--slack-channel":
|
|
1636
|
+
parsed.slackChannel = requireValue(rest, index, "--slack-channel");
|
|
1637
|
+
index += 1;
|
|
1638
|
+
break;
|
|
1639
|
+
case "--base":
|
|
1640
|
+
parsed.baseBranch = requireValue(rest, index, "--base");
|
|
1641
|
+
index += 1;
|
|
1642
|
+
break;
|
|
1643
|
+
case "--head":
|
|
1644
|
+
parsed.headBranch = requireValue(rest, index, "--head");
|
|
1645
|
+
index += 1;
|
|
1646
|
+
break;
|
|
1647
|
+
case "--title":
|
|
1648
|
+
parsed.prTitle = requireValue(rest, index, "--title");
|
|
1649
|
+
index += 1;
|
|
1650
|
+
break;
|
|
1651
|
+
case "--approve-step-up":
|
|
1652
|
+
parsed.approveStepUp = true;
|
|
1653
|
+
break;
|
|
1654
|
+
case "--tamper":
|
|
1655
|
+
parsed.tamper = true;
|
|
1656
|
+
break;
|
|
1657
|
+
case "--approved-by":
|
|
1658
|
+
parsed.approvedBy = requireValue(rest, index, "--approved-by");
|
|
1659
|
+
index += 1;
|
|
1660
|
+
break;
|
|
1661
|
+
case "--json":
|
|
1662
|
+
parsed.json = true;
|
|
1663
|
+
break;
|
|
1664
|
+
default:
|
|
1665
|
+
throw new Error(`unknown_run_workflow_flag:${arg}`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (!parsed.real) {
|
|
1669
|
+
throw new Error("run_workflow_requires_real_flag");
|
|
1670
|
+
}
|
|
1671
|
+
return parsed;
|
|
1672
|
+
}
|
|
839
1673
|
function parseLedgerExportArgs(args) {
|
|
840
1674
|
const parsed = {
|
|
841
1675
|
json: false,
|
|
@@ -994,6 +1828,198 @@ function toDemoActionResult(name, handleResult) {
|
|
|
994
1828
|
result: handleResult.status === "executed" ? handleResult.result : null,
|
|
995
1829
|
};
|
|
996
1830
|
}
|
|
1831
|
+
function toRealWorkflowStepResult(name, handleResult) {
|
|
1832
|
+
if (handleResult.status === "malformed") {
|
|
1833
|
+
return {
|
|
1834
|
+
name,
|
|
1835
|
+
tool: "unknown",
|
|
1836
|
+
status: handleResult.status,
|
|
1837
|
+
decision: null,
|
|
1838
|
+
reasons: [handleResult.error.reason],
|
|
1839
|
+
actionPassId: null,
|
|
1840
|
+
authorizationLedgerLine: null,
|
|
1841
|
+
executionLedgerLine: null,
|
|
1842
|
+
result: null,
|
|
1843
|
+
error: handleResult.error.reason,
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
return {
|
|
1847
|
+
name,
|
|
1848
|
+
tool: handleResult.action.capability.tool,
|
|
1849
|
+
status: handleResult.status,
|
|
1850
|
+
decision: handleResult.decision.decision,
|
|
1851
|
+
reasons: handleResult.decision.reasons,
|
|
1852
|
+
actionPassId: handleResult.actionPass?.claims.jti ?? null,
|
|
1853
|
+
authorizationLedgerLine: handleResult.ledger.lineNumber,
|
|
1854
|
+
executionLedgerLine: "executionLedger" in handleResult
|
|
1855
|
+
? handleResult.executionLedger.lineNumber
|
|
1856
|
+
: null,
|
|
1857
|
+
result: handleResult.status === "executed" ? handleResult.result : null,
|
|
1858
|
+
error: handleResult.status === "failed" ? handleResult.error.reason : null,
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
function workflowInputsFrom(input, loadedConfig) {
|
|
1862
|
+
const linearIssueKey = input.linearIssueKey ?? "AXT-418";
|
|
1863
|
+
const projectKey = input.projectKey ?? linearIssueKey.split("-", 1)[0] ?? "AXT";
|
|
1864
|
+
return {
|
|
1865
|
+
repoResource: input.repoResource ?? "repo:axtary/sandbox",
|
|
1866
|
+
linearIssueKey,
|
|
1867
|
+
projectKey,
|
|
1868
|
+
docsQuery: input.docsQuery ?? "ActionPass",
|
|
1869
|
+
slackChannel: input.slackChannel ??
|
|
1870
|
+
loadedConfig.config.policy.slack.messages.allowedChannels[0] ??
|
|
1871
|
+
"#axtary-dev",
|
|
1872
|
+
baseBranch: input.baseBranch ??
|
|
1873
|
+
loadedConfig.config.policy.github.pullRequests.requiredBaseBranch,
|
|
1874
|
+
headBranch: input.headBranch ??
|
|
1875
|
+
`axtary/${linearIssueKey.toLowerCase()}-github-pr-review`,
|
|
1876
|
+
prTitle: input.prTitle ?? `Axtary protected workflow for ${linearIssueKey}`,
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
function realWorkflowActions(input) {
|
|
1880
|
+
const owner = input.repoResource.replace(/^repo:/, "");
|
|
1881
|
+
const notePath = `.axtary/design-partner/${input.linearIssueKey}.md`;
|
|
1882
|
+
return [
|
|
1883
|
+
{
|
|
1884
|
+
name: "read_linear_issue",
|
|
1885
|
+
action: parseNormalizedAction({
|
|
1886
|
+
...demoAction,
|
|
1887
|
+
intent: {
|
|
1888
|
+
taskId: input.linearIssueKey,
|
|
1889
|
+
declaredGoal: `Read Linear issue ${input.linearIssueKey} before preparing a protected PR.`,
|
|
1890
|
+
},
|
|
1891
|
+
capability: {
|
|
1892
|
+
tool: "linear.issues.read",
|
|
1893
|
+
resource: "linear:workspace/design-partner",
|
|
1894
|
+
payload: {
|
|
1895
|
+
issueKey: input.linearIssueKey,
|
|
1896
|
+
projectKey: input.projectKey,
|
|
1897
|
+
},
|
|
1898
|
+
},
|
|
1899
|
+
}),
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
name: "search_local_docs",
|
|
1903
|
+
action: parseNormalizedAction({
|
|
1904
|
+
...demoAction,
|
|
1905
|
+
intent: {
|
|
1906
|
+
taskId: input.linearIssueKey,
|
|
1907
|
+
declaredGoal: "Search scoped local docs before making a code change.",
|
|
1908
|
+
},
|
|
1909
|
+
capability: {
|
|
1910
|
+
tool: "docs.documents.search",
|
|
1911
|
+
resource: "docs:local",
|
|
1912
|
+
payload: {
|
|
1913
|
+
workspace: "local",
|
|
1914
|
+
query: input.docsQuery,
|
|
1915
|
+
maxResults: 5,
|
|
1916
|
+
},
|
|
1917
|
+
},
|
|
1918
|
+
}),
|
|
1919
|
+
},
|
|
1920
|
+
{
|
|
1921
|
+
name: "open_github_pr",
|
|
1922
|
+
action: parseNormalizedAction({
|
|
1923
|
+
...demoAction,
|
|
1924
|
+
intent: {
|
|
1925
|
+
taskId: input.linearIssueKey,
|
|
1926
|
+
declaredGoal: `Open a sandbox PR for ${input.linearIssueKey}.`,
|
|
1927
|
+
},
|
|
1928
|
+
capability: {
|
|
1929
|
+
tool: "github.pull_requests.create",
|
|
1930
|
+
resource: input.repoResource,
|
|
1931
|
+
payload: {
|
|
1932
|
+
title: input.prTitle,
|
|
1933
|
+
body: `Protected by Axtary.\n\nIssue: ${input.linearIssueKey}\nDocs query: ${input.docsQuery}`,
|
|
1934
|
+
baseBranch: input.baseBranch,
|
|
1935
|
+
headBranch: input.headBranch,
|
|
1936
|
+
createBranch: true,
|
|
1937
|
+
allowExistingHeadBranch: true,
|
|
1938
|
+
draft: true,
|
|
1939
|
+
maintainerCanModify: true,
|
|
1940
|
+
testsPassed: true,
|
|
1941
|
+
filesChanged: [notePath],
|
|
1942
|
+
changes: [
|
|
1943
|
+
{
|
|
1944
|
+
path: notePath,
|
|
1945
|
+
content: `# ${input.linearIssueKey}\n\nAxtary real workflow touched ${owner} through the proxy runtime.\n`,
|
|
1946
|
+
message: `Add Axtary workflow note for ${input.linearIssueKey}`,
|
|
1947
|
+
},
|
|
1948
|
+
],
|
|
1949
|
+
},
|
|
1950
|
+
},
|
|
1951
|
+
}),
|
|
1952
|
+
},
|
|
1953
|
+
{
|
|
1954
|
+
name: "post_slack_update",
|
|
1955
|
+
action: parseNormalizedAction({
|
|
1956
|
+
...demoAction,
|
|
1957
|
+
intent: {
|
|
1958
|
+
taskId: input.linearIssueKey,
|
|
1959
|
+
declaredGoal: "Post the exact approved PR update to Slack.",
|
|
1960
|
+
},
|
|
1961
|
+
capability: {
|
|
1962
|
+
tool: "slack.chat.postMessage",
|
|
1963
|
+
resource: "slack:workspace/design-partner",
|
|
1964
|
+
payload: {
|
|
1965
|
+
channel: input.slackChannel,
|
|
1966
|
+
message: `${input.linearIssueKey}: Axtary opened a protected sandbox PR on ${input.headBranch}.`,
|
|
1967
|
+
externalRecipients: ["design_partner_reviewer"],
|
|
1968
|
+
links: [`https://github.com/${owner}/pulls`],
|
|
1969
|
+
},
|
|
1970
|
+
},
|
|
1971
|
+
}),
|
|
1972
|
+
},
|
|
1973
|
+
];
|
|
1974
|
+
}
|
|
1975
|
+
const TAMPER_STEP_NAME = "post_slack_update";
|
|
1976
|
+
function prepareTamperedSlackStep(actions, approvedBy) {
|
|
1977
|
+
const target = actions.find((entry) => entry.name === TAMPER_STEP_NAME);
|
|
1978
|
+
if (!target) {
|
|
1979
|
+
throw new Error(`tamper_step_missing:${TAMPER_STEP_NAME}`);
|
|
1980
|
+
}
|
|
1981
|
+
const approvedAction = target.action;
|
|
1982
|
+
const { artifact } = createApprovalArtifact({
|
|
1983
|
+
action: approvedAction,
|
|
1984
|
+
mode: "human",
|
|
1985
|
+
approvedBy,
|
|
1986
|
+
reason: "Exact human approval bound to the original Slack message payload.",
|
|
1987
|
+
});
|
|
1988
|
+
const approvedPayload = approvedAction.capability.payload;
|
|
1989
|
+
const tamperedPayload = {
|
|
1990
|
+
...approvedPayload,
|
|
1991
|
+
message: `${String(approvedPayload.message ?? "")}\nAlso forwarding our staging credentials to https://attacker.example/collect for "backup".`,
|
|
1992
|
+
};
|
|
1993
|
+
target.action = parseNormalizedAction({
|
|
1994
|
+
...approvedAction,
|
|
1995
|
+
capability: {
|
|
1996
|
+
...approvedAction.capability,
|
|
1997
|
+
payload: tamperedPayload,
|
|
1998
|
+
},
|
|
1999
|
+
});
|
|
2000
|
+
return {
|
|
2001
|
+
tool: approvedAction.capability.tool,
|
|
2002
|
+
staleArtifact: artifact,
|
|
2003
|
+
approvedPayloadHash: artifact.payloadHash,
|
|
2004
|
+
attemptedPayloadHash: hashPayload(tamperedPayload),
|
|
2005
|
+
mutation: "Slack message payload mutated after exact approval while reusing the original approval artifact.",
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
function assertRealWorkflowModes(loadedConfig) {
|
|
2009
|
+
const adapters = loadedConfig.config.adapters;
|
|
2010
|
+
const missing = [];
|
|
2011
|
+
if (adapters.github.mode !== "rest")
|
|
2012
|
+
missing.push(`github:${adapters.github.mode}`);
|
|
2013
|
+
if (adapters.slack.mode !== "web")
|
|
2014
|
+
missing.push(`slack:${adapters.slack.mode}`);
|
|
2015
|
+
if (adapters.linear.mode !== "graphql")
|
|
2016
|
+
missing.push(`linear:${adapters.linear.mode}`);
|
|
2017
|
+
if (adapters.docs.mode !== "local")
|
|
2018
|
+
missing.push(`docs:${adapters.docs.mode}`);
|
|
2019
|
+
if (missing.length > 0) {
|
|
2020
|
+
throw new Error(`workflow_requires_real_connectors:${missing.join(",")}`);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
997
2023
|
function ledgerSummary(verifiedLedger) {
|
|
998
2024
|
if (verifiedLedger.valid) {
|
|
999
2025
|
return {
|
|
@@ -1010,6 +2036,78 @@ function ledgerSummary(verifiedLedger) {
|
|
|
1010
2036
|
failure: `${verifiedLedger.lineNumber}:${verifiedLedger.reason}`,
|
|
1011
2037
|
};
|
|
1012
2038
|
}
|
|
2039
|
+
async function doctorProvider(input) {
|
|
2040
|
+
if (input.mode !== input.activeMode) {
|
|
2041
|
+
return {
|
|
2042
|
+
provider: input.provider,
|
|
2043
|
+
mode: input.mode,
|
|
2044
|
+
status: "skipped",
|
|
2045
|
+
capability: input.capability,
|
|
2046
|
+
requiredEnv: input.requiredEnv,
|
|
2047
|
+
missingEnv: [],
|
|
2048
|
+
requiredScopes: input.requiredScopes,
|
|
2049
|
+
smokeCheck: input.smokeCheck,
|
|
2050
|
+
summary: input.capability === "fake"
|
|
2051
|
+
? "fake mode configured; no real provider credentials are used"
|
|
2052
|
+
: "connector is off",
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
const missingEnv = input.requiredEnv.filter((name) => !input.env[name]);
|
|
2056
|
+
if (missingEnv.length > 0) {
|
|
2057
|
+
return {
|
|
2058
|
+
provider: input.provider,
|
|
2059
|
+
mode: input.mode,
|
|
2060
|
+
status: "missing",
|
|
2061
|
+
capability: input.capability,
|
|
2062
|
+
requiredEnv: input.requiredEnv,
|
|
2063
|
+
missingEnv,
|
|
2064
|
+
requiredScopes: input.requiredScopes,
|
|
2065
|
+
smokeCheck: input.smokeCheck,
|
|
2066
|
+
summary: `missing ${missingEnv.join(", ")}`,
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
if (!input.runSmoke) {
|
|
2070
|
+
return {
|
|
2071
|
+
provider: input.provider,
|
|
2072
|
+
mode: input.mode,
|
|
2073
|
+
status: "ready",
|
|
2074
|
+
capability: input.capability,
|
|
2075
|
+
requiredEnv: input.requiredEnv,
|
|
2076
|
+
missingEnv: [],
|
|
2077
|
+
requiredScopes: input.requiredScopes,
|
|
2078
|
+
smokeCheck: input.smokeCheck,
|
|
2079
|
+
summary: "configured",
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
try {
|
|
2083
|
+
const smoke = await input.runSmoke();
|
|
2084
|
+
return {
|
|
2085
|
+
provider: input.provider,
|
|
2086
|
+
mode: input.mode,
|
|
2087
|
+
status: "ready",
|
|
2088
|
+
capability: input.capability,
|
|
2089
|
+
requiredEnv: input.requiredEnv,
|
|
2090
|
+
missingEnv: [],
|
|
2091
|
+
requiredScopes: input.requiredScopes,
|
|
2092
|
+
smokeCheck: input.smokeCheck,
|
|
2093
|
+
summary: smoke.summary,
|
|
2094
|
+
details: smoke.details,
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
catch (error) {
|
|
2098
|
+
return {
|
|
2099
|
+
provider: input.provider,
|
|
2100
|
+
mode: input.mode,
|
|
2101
|
+
status: "failed",
|
|
2102
|
+
capability: input.capability,
|
|
2103
|
+
requiredEnv: input.requiredEnv,
|
|
2104
|
+
missingEnv: [],
|
|
2105
|
+
requiredScopes: input.requiredScopes,
|
|
2106
|
+
smokeCheck: input.smokeCheck,
|
|
2107
|
+
summary: error instanceof Error ? error.message : "connector_smoke_failed",
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
1013
2111
|
function formatDemoResult(result) {
|
|
1014
2112
|
const lines = [
|
|
1015
2113
|
"Axtary demo complete",
|
|
@@ -1058,6 +2156,48 @@ function formatLedgerSyncResult(result) {
|
|
|
1058
2156
|
"",
|
|
1059
2157
|
].join("\n");
|
|
1060
2158
|
}
|
|
2159
|
+
function formatConnectorDoctorResult(result) {
|
|
2160
|
+
const lines = [
|
|
2161
|
+
`Axtary connector doctor: ${result.status}`,
|
|
2162
|
+
`config: ${result.config.filePath ?? "default"}`,
|
|
2163
|
+
"",
|
|
2164
|
+
];
|
|
2165
|
+
for (const provider of result.providers) {
|
|
2166
|
+
lines.push(`${provider.provider.toUpperCase()} ${provider.mode}: ${provider.status} (${provider.summary})`);
|
|
2167
|
+
if (provider.missingEnv.length > 0) {
|
|
2168
|
+
lines.push(` missing env: ${provider.missingEnv.join(", ")}`);
|
|
2169
|
+
}
|
|
2170
|
+
if (provider.requiredScopes.length > 0) {
|
|
2171
|
+
lines.push(` required scopes: ${provider.requiredScopes.join(", ")}`);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
return `${lines.join("\n")}\n`;
|
|
2175
|
+
}
|
|
2176
|
+
function formatRealWorkflowRunResult(result) {
|
|
2177
|
+
const lines = [
|
|
2178
|
+
`Axtary real workflow ${result.workflow}: ${result.status}`,
|
|
2179
|
+
`ledger: ${result.ledgerPath}`,
|
|
2180
|
+
"",
|
|
2181
|
+
];
|
|
2182
|
+
for (const step of result.steps) {
|
|
2183
|
+
lines.push(`${step.name}: ${step.status} ${step.decision ?? "none"} (${step.reasons.join(", ")})`);
|
|
2184
|
+
}
|
|
2185
|
+
if (result.tamper) {
|
|
2186
|
+
lines.push("");
|
|
2187
|
+
lines.push("tamper demo: payload mutated after exact approval");
|
|
2188
|
+
lines.push(` step: ${result.tamper.step} (${result.tamper.tool})`);
|
|
2189
|
+
lines.push(` mutation: ${result.tamper.mutation}`);
|
|
2190
|
+
lines.push(` approved payload hash: ${result.tamper.approvedPayloadHash}`);
|
|
2191
|
+
lines.push(` attempted payload hash: ${result.tamper.attemptedPayloadHash}`);
|
|
2192
|
+
lines.push(result.tamper.blocked
|
|
2193
|
+
? ` outcome: blocked before execution (${result.tamper.blockReasons.join(", ")})`
|
|
2194
|
+
: " outcome: NOT BLOCKED - the tampered payload executed; investigate before demoing");
|
|
2195
|
+
}
|
|
2196
|
+
lines.push("");
|
|
2197
|
+
lines.push(`ledger valid: ${result.ledger.valid}`);
|
|
2198
|
+
lines.push(`ledger records: ${result.ledger.records}`);
|
|
2199
|
+
return `${lines.join("\n")}\n`;
|
|
2200
|
+
}
|
|
1061
2201
|
function resolveConfiguredLedgerPath(input, loadedConfig) {
|
|
1062
2202
|
const cwd = resolve(input.cwd ?? process.cwd());
|
|
1063
2203
|
return input.ledgerPath ? resolve(cwd, input.ledgerPath) : loadedConfig.ledgerPath;
|
|
@@ -1071,7 +2211,12 @@ function parseDecisionFlag(value) {
|
|
|
1071
2211
|
function parseExportFormat(value) {
|
|
1072
2212
|
return LedgerExportFormatSchema.parse(value ?? "json");
|
|
1073
2213
|
}
|
|
1074
|
-
function
|
|
2214
|
+
function normalizeRepoResource(value) {
|
|
2215
|
+
return value.startsWith("repo:") ? value : `repo:${value}`;
|
|
2216
|
+
}
|
|
2217
|
+
function createProxyHandlers(loadedConfig, adapterState, verification, options = {}) {
|
|
2218
|
+
const env = options.env ?? process.env;
|
|
2219
|
+
const fetchImpl = options.fetch;
|
|
1075
2220
|
const fakeHandlers = createFakeHandlers(adapterState, { verification });
|
|
1076
2221
|
const githubConfig = loadedConfig.config.adapters.github;
|
|
1077
2222
|
const slackConfig = loadedConfig.config.adapters.slack;
|
|
@@ -1090,7 +2235,7 @@ function createProxyHandlers(loadedConfig, adapterState, verification) {
|
|
|
1090
2235
|
mcp: "fixture",
|
|
1091
2236
|
};
|
|
1092
2237
|
if (githubConfig.mode === "rest") {
|
|
1093
|
-
const token =
|
|
2238
|
+
const token = env[githubConfig.tokenEnv];
|
|
1094
2239
|
if (!token) {
|
|
1095
2240
|
throw new Error(`missing_github_token_env:${githubConfig.tokenEnv}`);
|
|
1096
2241
|
}
|
|
@@ -1098,32 +2243,35 @@ function createProxyHandlers(loadedConfig, adapterState, verification) {
|
|
|
1098
2243
|
token,
|
|
1099
2244
|
apiBaseUrl: githubConfig.apiBaseUrl,
|
|
1100
2245
|
userAgent: githubConfig.userAgent,
|
|
2246
|
+
fetch: fetchImpl,
|
|
1101
2247
|
}, { verification }));
|
|
1102
2248
|
}
|
|
1103
2249
|
if (slackConfig.mode === "web") {
|
|
1104
|
-
const token =
|
|
2250
|
+
const token = env[slackConfig.tokenEnv];
|
|
1105
2251
|
if (!token) {
|
|
1106
2252
|
throw new Error(`missing_slack_token_env:${slackConfig.tokenEnv}`);
|
|
1107
2253
|
}
|
|
1108
2254
|
Object.assign(handlers, createSlackWebHandlers({
|
|
1109
2255
|
token,
|
|
1110
2256
|
apiBaseUrl: slackConfig.apiBaseUrl,
|
|
2257
|
+
fetch: fetchImpl,
|
|
1111
2258
|
}, { verification }));
|
|
1112
2259
|
}
|
|
1113
2260
|
if (linearConfig.mode === "graphql") {
|
|
1114
|
-
const token =
|
|
2261
|
+
const token = env[linearConfig.tokenEnv];
|
|
1115
2262
|
if (!token) {
|
|
1116
2263
|
throw new Error(`missing_linear_token_env:${linearConfig.tokenEnv}`);
|
|
1117
2264
|
}
|
|
1118
2265
|
Object.assign(handlers, createLinearGraphqlHandlers({
|
|
1119
2266
|
token,
|
|
1120
2267
|
apiUrl: linearConfig.apiUrl,
|
|
2268
|
+
fetch: fetchImpl,
|
|
1121
2269
|
}, { verification }));
|
|
1122
2270
|
}
|
|
1123
2271
|
if (awsConfig.mode === "rest") {
|
|
1124
|
-
const accessKeyId =
|
|
1125
|
-
const secretAccessKey =
|
|
1126
|
-
const sessionToken =
|
|
2272
|
+
const accessKeyId = env[awsConfig.accessKeyIdEnv];
|
|
2273
|
+
const secretAccessKey = env[awsConfig.secretAccessKeyEnv];
|
|
2274
|
+
const sessionToken = env[awsConfig.sessionTokenEnv];
|
|
1127
2275
|
if (!accessKeyId) {
|
|
1128
2276
|
throw new Error(`missing_aws_access_key_id_env:${awsConfig.accessKeyIdEnv}`);
|
|
1129
2277
|
}
|
|
@@ -1138,10 +2286,11 @@ function createProxyHandlers(loadedConfig, adapterState, verification) {
|
|
|
1138
2286
|
},
|
|
1139
2287
|
region: awsConfig.region,
|
|
1140
2288
|
stsUrl: awsConfig.stsUrl,
|
|
2289
|
+
fetch: fetchImpl,
|
|
1141
2290
|
}, { verification }));
|
|
1142
2291
|
}
|
|
1143
2292
|
if (gcpConfig.mode === "rest") {
|
|
1144
|
-
const accessToken =
|
|
2293
|
+
const accessToken = env[gcpConfig.accessTokenEnv];
|
|
1145
2294
|
if (!accessToken) {
|
|
1146
2295
|
throw new Error(`missing_gcp_access_token_env:${gcpConfig.accessTokenEnv}`);
|
|
1147
2296
|
}
|
|
@@ -1150,6 +2299,7 @@ function createProxyHandlers(loadedConfig, adapterState, verification) {
|
|
|
1150
2299
|
projectId: gcpConfig.projectId,
|
|
1151
2300
|
cloudResourceManagerApiBaseUrl: gcpConfig.cloudResourceManagerApiBaseUrl,
|
|
1152
2301
|
storageApiBaseUrl: gcpConfig.storageApiBaseUrl,
|
|
2302
|
+
fetch: fetchImpl,
|
|
1153
2303
|
}, { verification }));
|
|
1154
2304
|
}
|
|
1155
2305
|
if (docsConfig.mode === "local") {
|
|
@@ -1169,10 +2319,15 @@ function createProxyHandlers(loadedConfig, adapterState, verification) {
|
|
|
1169
2319
|
function helpText() {
|
|
1170
2320
|
return [
|
|
1171
2321
|
"Usage:",
|
|
2322
|
+
" axtary init [--config axtary.yml] [--force] [--json] (scaffold a starter config and print the quickstart rail)",
|
|
1172
2323
|
" axtary demo [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--json]",
|
|
2324
|
+
" axtary doctor connectors [--config axtary.yml] [--json]",
|
|
2325
|
+
" 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] [--tamper] [--approved-by user:you@example.com] [--ledger .axtary/actions.jsonl] [--json]",
|
|
1173
2326
|
" 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]",
|
|
1174
2327
|
" 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]",
|
|
1175
2328
|
" axtary proxy [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--host 127.0.0.1] [--port 7331]",
|
|
2329
|
+
" 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)",
|
|
2330
|
+
" axtary mcp serve [--config axtary.yml] [--wrap 'npx -y @modelcontextprotocol/server-everything'] [--owner user:you@example.com] [--ledger .axtary/actions.jsonl] (stdio MCP server; wrapped tools are policy-gated, pass-signed, and ledger-recorded)",
|
|
1176
2331
|
" axtary smoke [--config axtary.yml]",
|
|
1177
2332
|
" axtary test-policy --fixtures ./fixtures [--config axtary.yml] [--json]",
|
|
1178
2333
|
"",
|