@axtary/cli 0.0.1
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 +35 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1275 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1275 @@
|
|
|
1
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { extname, resolve } from "node:path";
|
|
5
|
+
import { AxtaryDecisionSchema, demoAction, parseNormalizedAction, } from "@axtary/actionpass";
|
|
6
|
+
import { createFakeAdapterState, createFakeHandlers, createAwsRestHandlers, createGcpRestHandlers, createGitHubRestHandlers, createLinearGraphqlHandlers, createLocalDocsHandlers, createSlackWebHandlers, smokeAwsRestCredentials, smokeGcpRestCredentials, smokeGitHubRestCredentials, smokeLinearGraphqlCredentials, smokeLocalDocsRoots, smokeSlackWebCredentials, } from "@axtary/adapters";
|
|
7
|
+
import { loadAxtaryConfig, CachedConfigLoader } from "@axtary/config";
|
|
8
|
+
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";
|
|
11
|
+
import { createProxyRuntime, } from "@axtary/proxy";
|
|
12
|
+
export const DEMO_SCHEMA_VERSION = "axtary.demo.v0";
|
|
13
|
+
export const POLICY_TEST_SCHEMA_VERSION = "axtary.policy_test.v0";
|
|
14
|
+
export const LOCAL_PROXY_SCHEMA_VERSION = "axtary.local_proxy.v0";
|
|
15
|
+
export const LEDGER_EXPORT_RUN_SCHEMA_VERSION = "axtary.ledger_export_run.v0";
|
|
16
|
+
export const LEDGER_SYNC_RUN_SCHEMA_VERSION = "axtary.ledger_sync_run.v0";
|
|
17
|
+
const MCP_DEMO_TOOL_DEFINITION = createMcpToolDefinition({
|
|
18
|
+
serverIdentity: "mcp:axtary-demo-fixtures",
|
|
19
|
+
serverVersion: "1.0.0",
|
|
20
|
+
schemaVersion: "2026-06-03",
|
|
21
|
+
name: "read_fixture",
|
|
22
|
+
description: "Read a deterministic demo fixture through a local MCP wrapper.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
properties: {
|
|
27
|
+
fixtureKey: {
|
|
28
|
+
type: "string",
|
|
29
|
+
enum: ["repo_primer"],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ["fixtureKey"],
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const MCP_DEMO_FIXTURES = {
|
|
36
|
+
repo_primer: {
|
|
37
|
+
path: "README.md",
|
|
38
|
+
mediaType: "text/markdown",
|
|
39
|
+
content: "Axtary protects agent tool calls by pinning policy to a signed action, scoped connector, and tamper-evident ledger record.",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const DEMO_ACTIONS = [
|
|
43
|
+
{
|
|
44
|
+
name: "safe_github_pr",
|
|
45
|
+
action: parseNormalizedAction({
|
|
46
|
+
...demoAction,
|
|
47
|
+
capability: {
|
|
48
|
+
...demoAction.capability,
|
|
49
|
+
payload: {
|
|
50
|
+
...demoAction.capability.payload,
|
|
51
|
+
title: "Fix auth redirect",
|
|
52
|
+
testsPassed: true,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "protected_github_pr",
|
|
59
|
+
action: parseNormalizedAction({
|
|
60
|
+
...demoAction,
|
|
61
|
+
intent: {
|
|
62
|
+
...demoAction.intent,
|
|
63
|
+
taskId: "AXT-419",
|
|
64
|
+
declaredGoal: "Touch production Terraform from a coding agent",
|
|
65
|
+
},
|
|
66
|
+
capability: {
|
|
67
|
+
...demoAction.capability,
|
|
68
|
+
payload: {
|
|
69
|
+
baseBranch: "main",
|
|
70
|
+
filesChanged: ["infra/prod/main.tf"],
|
|
71
|
+
testsPassed: true,
|
|
72
|
+
touchesProduction: true,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "safe_slack_message",
|
|
79
|
+
action: parseNormalizedAction({
|
|
80
|
+
...demoAction,
|
|
81
|
+
intent: {
|
|
82
|
+
...demoAction.intent,
|
|
83
|
+
taskId: "AXT-420",
|
|
84
|
+
declaredGoal: "Post deployment note to approved engineering channel",
|
|
85
|
+
},
|
|
86
|
+
capability: {
|
|
87
|
+
tool: "slack.chat.postMessage",
|
|
88
|
+
resource: "slack:workspace/company",
|
|
89
|
+
payload: {
|
|
90
|
+
channel: "#axtary-dev",
|
|
91
|
+
message: "Demo PR is ready for review.",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "wrong_slack_channel",
|
|
98
|
+
action: parseNormalizedAction({
|
|
99
|
+
...demoAction,
|
|
100
|
+
intent: {
|
|
101
|
+
...demoAction.intent,
|
|
102
|
+
taskId: "AXT-421",
|
|
103
|
+
declaredGoal: "Post internal deployment details to a broad channel",
|
|
104
|
+
},
|
|
105
|
+
capability: {
|
|
106
|
+
tool: "slack.chat.postMessage",
|
|
107
|
+
resource: "slack:workspace/company",
|
|
108
|
+
payload: {
|
|
109
|
+
channel: "#general",
|
|
110
|
+
message: "Deploying production auth changes now.",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "safe_linear_comment",
|
|
117
|
+
action: parseNormalizedAction({
|
|
118
|
+
...demoAction,
|
|
119
|
+
intent: {
|
|
120
|
+
...demoAction.intent,
|
|
121
|
+
taskId: "AXT-418",
|
|
122
|
+
declaredGoal: "Update the source Linear issue after opening the demo PR",
|
|
123
|
+
},
|
|
124
|
+
capability: {
|
|
125
|
+
tool: "linear.comments.create",
|
|
126
|
+
resource: "linear:workspace/company",
|
|
127
|
+
payload: {
|
|
128
|
+
issueKey: "AXT-418",
|
|
129
|
+
projectKey: "AXT",
|
|
130
|
+
body: "Axtary demo PR passed local policy and was recorded in the ledger.",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "safe_jira_read",
|
|
137
|
+
action: parseNormalizedAction({
|
|
138
|
+
...demoAction,
|
|
139
|
+
intent: {
|
|
140
|
+
...demoAction.intent,
|
|
141
|
+
taskId: "AXT-772",
|
|
142
|
+
declaredGoal: "Read a Jira issue inside the approved project scope",
|
|
143
|
+
},
|
|
144
|
+
capability: {
|
|
145
|
+
tool: "jira.issues.read",
|
|
146
|
+
resource: "jira:site/company",
|
|
147
|
+
payload: {
|
|
148
|
+
issueKey: "AXT-772",
|
|
149
|
+
projectKey: "AXT",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "protected_linear_done_status",
|
|
156
|
+
action: parseNormalizedAction({
|
|
157
|
+
...demoAction,
|
|
158
|
+
intent: {
|
|
159
|
+
...demoAction.intent,
|
|
160
|
+
taskId: "AXT-418",
|
|
161
|
+
declaredGoal: "Mark an agent-owned issue complete",
|
|
162
|
+
},
|
|
163
|
+
capability: {
|
|
164
|
+
tool: "linear.issues.update",
|
|
165
|
+
resource: "linear:workspace/company",
|
|
166
|
+
payload: {
|
|
167
|
+
issueKey: "AXT-418",
|
|
168
|
+
projectKey: "AXT",
|
|
169
|
+
status: "Done",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "safe_mcp_fixture_read",
|
|
176
|
+
action: createMcpAction({
|
|
177
|
+
definition: MCP_DEMO_TOOL_DEFINITION,
|
|
178
|
+
actor: demoAction.actor,
|
|
179
|
+
intent: {
|
|
180
|
+
...demoAction.intent,
|
|
181
|
+
taskId: "AXT-MCP-1",
|
|
182
|
+
declaredGoal: "Read a pinned local MCP fixture without exposing broad tool access",
|
|
183
|
+
},
|
|
184
|
+
call: {
|
|
185
|
+
toolName: MCP_DEMO_TOOL_DEFINITION.name,
|
|
186
|
+
arguments: {
|
|
187
|
+
fixtureKey: "repo_primer",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
constraints: {
|
|
191
|
+
demoOnly: true,
|
|
192
|
+
transport: "fixture",
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
export async function startProxyServer(input = {}) {
|
|
198
|
+
const configLoader = new CachedConfigLoader({
|
|
199
|
+
filePath: input.configPath,
|
|
200
|
+
cwd: input.cwd,
|
|
201
|
+
});
|
|
202
|
+
const loadedConfig = await configLoader.getConfig();
|
|
203
|
+
const cwd = resolve(input.cwd ?? process.cwd());
|
|
204
|
+
const ledgerPath = input.ledgerPath
|
|
205
|
+
? resolve(cwd, input.ledgerPath)
|
|
206
|
+
: loadedConfig.ledgerPath;
|
|
207
|
+
const ledger = new LocalJsonlLedger(ledgerPath);
|
|
208
|
+
const adapterState = createFakeAdapterState();
|
|
209
|
+
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
210
|
+
const { handlers, adapterModes } = createProxyHandlers(loadedConfig, adapterState, {
|
|
211
|
+
issuer: loadedConfig.config.issuer,
|
|
212
|
+
verificationKey: publicKey,
|
|
213
|
+
});
|
|
214
|
+
const runtime = createProxyRuntime({
|
|
215
|
+
issuer: loadedConfig.config.issuer,
|
|
216
|
+
tenant: loadedConfig.config.tenant,
|
|
217
|
+
ledger,
|
|
218
|
+
policy: async () => (await configLoader.getConfig()).config.policy,
|
|
219
|
+
signingKey: privateKey,
|
|
220
|
+
keyId: "local-proxy-key",
|
|
221
|
+
allowUnsignedExecution: loadedConfig.config.runtime.allowUnsignedExecution,
|
|
222
|
+
handlerTimeoutMs: loadedConfig.config.runtime.handlerTimeoutMs,
|
|
223
|
+
handlers,
|
|
224
|
+
});
|
|
225
|
+
const host = input.host ?? "127.0.0.1";
|
|
226
|
+
const server = createServer(async (request, response) => {
|
|
227
|
+
try {
|
|
228
|
+
if (request.method === "GET" && request.url === "/health") {
|
|
229
|
+
writeJson(response, 200, {
|
|
230
|
+
schemaVersion: LOCAL_PROXY_SCHEMA_VERSION,
|
|
231
|
+
status: "ok",
|
|
232
|
+
configPath: loadedConfig.filePath,
|
|
233
|
+
ledgerPath,
|
|
234
|
+
adapterModes,
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (request.method === "GET" && request.url === "/state") {
|
|
239
|
+
writeJson(response, 200, {
|
|
240
|
+
schemaVersion: LOCAL_PROXY_SCHEMA_VERSION,
|
|
241
|
+
adapters: adapterState,
|
|
242
|
+
adapterModes,
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (request.method === "POST" && request.url === "/actions") {
|
|
247
|
+
const body = await readJsonBody(request);
|
|
248
|
+
const result = await runtime.handle(body);
|
|
249
|
+
const statusCode = result.status === "malformed" ? 400 : 200;
|
|
250
|
+
writeJson(response, statusCode, result);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
writeJson(response, 404, {
|
|
254
|
+
schemaVersion: LOCAL_PROXY_SCHEMA_VERSION,
|
|
255
|
+
error: "not_found",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
writeJson(response, 500, {
|
|
260
|
+
schemaVersion: LOCAL_PROXY_SCHEMA_VERSION,
|
|
261
|
+
error: error instanceof Error ? error.message : "proxy_request_failed",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
await listen(server, input.port ?? 7331, host);
|
|
266
|
+
const address = server.address();
|
|
267
|
+
const port = typeof address === "object" && address !== null ? address.port : input.port ?? 7331;
|
|
268
|
+
return {
|
|
269
|
+
schemaVersion: LOCAL_PROXY_SCHEMA_VERSION,
|
|
270
|
+
url: `http://${host}:${port}`,
|
|
271
|
+
host,
|
|
272
|
+
port,
|
|
273
|
+
ledgerPath,
|
|
274
|
+
adapters: adapterState,
|
|
275
|
+
adapterModes,
|
|
276
|
+
close: () => close(server),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export async function runDemo(input = {}) {
|
|
280
|
+
const loadedConfig = await loadAxtaryConfig({
|
|
281
|
+
filePath: input.configPath,
|
|
282
|
+
cwd: input.cwd,
|
|
283
|
+
});
|
|
284
|
+
const cwd = resolve(input.cwd ?? process.cwd());
|
|
285
|
+
const ledgerPath = input.ledgerPath
|
|
286
|
+
? resolve(cwd, input.ledgerPath)
|
|
287
|
+
: loadedConfig.ledgerPath;
|
|
288
|
+
const ledger = new LocalJsonlLedger(ledgerPath);
|
|
289
|
+
const adapterState = createFakeAdapterState();
|
|
290
|
+
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
291
|
+
const demoPolicy = {
|
|
292
|
+
...loadedConfig.config.policy,
|
|
293
|
+
mcp: {
|
|
294
|
+
...loadedConfig.config.policy.mcp,
|
|
295
|
+
allowedTools: [
|
|
296
|
+
...(loadedConfig.config.policy.mcp.allowedTools ?? []),
|
|
297
|
+
{
|
|
298
|
+
serverIdentity: MCP_DEMO_TOOL_DEFINITION.serverIdentity,
|
|
299
|
+
toolName: MCP_DEMO_TOOL_DEFINITION.name,
|
|
300
|
+
definitionHash: MCP_DEMO_TOOL_DEFINITION.definitionHash,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
const fakeHandlers = createFakeHandlers(adapterState, {
|
|
306
|
+
verification: {
|
|
307
|
+
issuer: loadedConfig.config.issuer,
|
|
308
|
+
verificationKey: publicKey,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
const runtime = createProxyRuntime({
|
|
312
|
+
issuer: loadedConfig.config.issuer,
|
|
313
|
+
tenant: loadedConfig.config.tenant,
|
|
314
|
+
ledger,
|
|
315
|
+
policy: demoPolicy,
|
|
316
|
+
signingKey: privateKey,
|
|
317
|
+
keyId: "local-demo-key",
|
|
318
|
+
allowUnsignedExecution: loadedConfig.config.runtime.allowUnsignedExecution,
|
|
319
|
+
handlerTimeoutMs: loadedConfig.config.runtime.handlerTimeoutMs,
|
|
320
|
+
handlers: {
|
|
321
|
+
...fakeHandlers,
|
|
322
|
+
[MCP_TOOL_CALL]: createMcpFixtureToolHandler({
|
|
323
|
+
definition: MCP_DEMO_TOOL_DEFINITION,
|
|
324
|
+
fixtures: MCP_DEMO_FIXTURES,
|
|
325
|
+
}),
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
const results = [];
|
|
329
|
+
for (const demo of DEMO_ACTIONS) {
|
|
330
|
+
const result = await runtime.handle(demo.action);
|
|
331
|
+
results.push(toDemoActionResult(demo.name, result));
|
|
332
|
+
}
|
|
333
|
+
const verifiedLedger = await ledger.verify();
|
|
334
|
+
return {
|
|
335
|
+
schemaVersion: DEMO_SCHEMA_VERSION,
|
|
336
|
+
config: {
|
|
337
|
+
filePath: loadedConfig.filePath,
|
|
338
|
+
issuer: loadedConfig.config.issuer,
|
|
339
|
+
tenant: loadedConfig.config.tenant,
|
|
340
|
+
},
|
|
341
|
+
ledgerPath,
|
|
342
|
+
results,
|
|
343
|
+
adapters: adapterState,
|
|
344
|
+
adapterModes: {
|
|
345
|
+
github: "fake",
|
|
346
|
+
slack: "fake",
|
|
347
|
+
linear: "fake",
|
|
348
|
+
aws: "off",
|
|
349
|
+
gcp: "off",
|
|
350
|
+
mcp: "fixture",
|
|
351
|
+
},
|
|
352
|
+
ledger: ledgerSummary(verifiedLedger),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
export async function runSmokeTest(input = {}, io = {}) {
|
|
356
|
+
const stdout = io.stdout ?? ((text) => process.stdout.write(text));
|
|
357
|
+
const env = input.env ?? process.env;
|
|
358
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
359
|
+
const loadedConfig = await loadAxtaryConfig({
|
|
360
|
+
filePath: input.configPath,
|
|
361
|
+
cwd: input.cwd,
|
|
362
|
+
});
|
|
363
|
+
let failed = false;
|
|
364
|
+
const githubConfig = loadedConfig.config.adapters.github;
|
|
365
|
+
if (githubConfig.mode === "rest") {
|
|
366
|
+
stdout("Testing GitHub REST credentials... ");
|
|
367
|
+
const token = env[githubConfig.tokenEnv];
|
|
368
|
+
if (!token) {
|
|
369
|
+
stdout(`FAIL (missing ${githubConfig.tokenEnv})\n`);
|
|
370
|
+
failed = true;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
try {
|
|
374
|
+
const result = await smokeGitHubRestCredentials({
|
|
375
|
+
token,
|
|
376
|
+
apiBaseUrl: githubConfig.apiBaseUrl,
|
|
377
|
+
userAgent: githubConfig.userAgent,
|
|
378
|
+
fetch: fetchImpl,
|
|
379
|
+
maxRetries: 0,
|
|
380
|
+
});
|
|
381
|
+
stdout(`OK (${result.summary})\n`);
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
stdout(`FAIL (${e instanceof Error ? e.message : String(e)})\n`);
|
|
385
|
+
failed = true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
stdout("Skipping GitHub (not in rest mode)\n");
|
|
391
|
+
}
|
|
392
|
+
const slackConfig = loadedConfig.config.adapters.slack;
|
|
393
|
+
if (slackConfig.mode === "web") {
|
|
394
|
+
stdout("Testing Slack Web credentials... ");
|
|
395
|
+
const token = env[slackConfig.tokenEnv];
|
|
396
|
+
if (!token) {
|
|
397
|
+
stdout(`FAIL (missing ${slackConfig.tokenEnv})\n`);
|
|
398
|
+
failed = true;
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
try {
|
|
402
|
+
const result = await smokeSlackWebCredentials({
|
|
403
|
+
token,
|
|
404
|
+
apiBaseUrl: slackConfig.apiBaseUrl,
|
|
405
|
+
fetch: fetchImpl,
|
|
406
|
+
maxRetries: 0,
|
|
407
|
+
});
|
|
408
|
+
stdout(`OK (${result.summary})\n`);
|
|
409
|
+
}
|
|
410
|
+
catch (e) {
|
|
411
|
+
stdout(`FAIL (${e instanceof Error ? e.message : String(e)})\n`);
|
|
412
|
+
failed = true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
stdout("Skipping Slack (not in web mode)\n");
|
|
418
|
+
}
|
|
419
|
+
const linearConfig = loadedConfig.config.adapters.linear;
|
|
420
|
+
if (linearConfig.mode === "graphql") {
|
|
421
|
+
stdout("Testing Linear GraphQL credentials... ");
|
|
422
|
+
const token = env[linearConfig.tokenEnv];
|
|
423
|
+
if (!token) {
|
|
424
|
+
stdout(`FAIL (missing ${linearConfig.tokenEnv})\n`);
|
|
425
|
+
failed = true;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
try {
|
|
429
|
+
const result = await smokeLinearGraphqlCredentials({
|
|
430
|
+
token,
|
|
431
|
+
apiUrl: linearConfig.apiUrl,
|
|
432
|
+
fetch: fetchImpl,
|
|
433
|
+
maxRetries: 0,
|
|
434
|
+
});
|
|
435
|
+
stdout(`OK (${result.summary})\n`);
|
|
436
|
+
}
|
|
437
|
+
catch (e) {
|
|
438
|
+
stdout(`FAIL (${e instanceof Error ? e.message : String(e)})\n`);
|
|
439
|
+
failed = true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
stdout("Skipping Linear (not in graphql mode)\n");
|
|
445
|
+
}
|
|
446
|
+
const awsConfig = loadedConfig.config.adapters.aws;
|
|
447
|
+
if (awsConfig.mode === "rest") {
|
|
448
|
+
stdout("Testing AWS REST credentials... ");
|
|
449
|
+
const accessKeyId = env[awsConfig.accessKeyIdEnv];
|
|
450
|
+
const secretAccessKey = env[awsConfig.secretAccessKeyEnv];
|
|
451
|
+
const sessionToken = env[awsConfig.sessionTokenEnv];
|
|
452
|
+
if (!accessKeyId) {
|
|
453
|
+
stdout(`FAIL (missing ${awsConfig.accessKeyIdEnv})\n`);
|
|
454
|
+
failed = true;
|
|
455
|
+
}
|
|
456
|
+
else if (!secretAccessKey) {
|
|
457
|
+
stdout(`FAIL (missing ${awsConfig.secretAccessKeyEnv})\n`);
|
|
458
|
+
failed = true;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
try {
|
|
462
|
+
const result = await smokeAwsRestCredentials({
|
|
463
|
+
credentials: {
|
|
464
|
+
accessKeyId,
|
|
465
|
+
secretAccessKey,
|
|
466
|
+
sessionToken,
|
|
467
|
+
},
|
|
468
|
+
region: awsConfig.region,
|
|
469
|
+
stsUrl: awsConfig.stsUrl,
|
|
470
|
+
fetch: fetchImpl,
|
|
471
|
+
maxRetries: 0,
|
|
472
|
+
});
|
|
473
|
+
stdout(`OK (${result.summary})\n`);
|
|
474
|
+
}
|
|
475
|
+
catch (e) {
|
|
476
|
+
stdout(`FAIL (${e instanceof Error ? e.message : String(e)})\n`);
|
|
477
|
+
failed = true;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
stdout("Skipping AWS (not in rest mode)\n");
|
|
483
|
+
}
|
|
484
|
+
const gcpConfig = loadedConfig.config.adapters.gcp;
|
|
485
|
+
if (gcpConfig.mode === "rest") {
|
|
486
|
+
stdout("Testing GCP REST credentials... ");
|
|
487
|
+
const accessToken = env[gcpConfig.accessTokenEnv];
|
|
488
|
+
if (!accessToken) {
|
|
489
|
+
stdout(`FAIL (missing ${gcpConfig.accessTokenEnv})\n`);
|
|
490
|
+
failed = true;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
try {
|
|
494
|
+
const result = await smokeGcpRestCredentials({
|
|
495
|
+
accessToken,
|
|
496
|
+
projectId: gcpConfig.projectId,
|
|
497
|
+
cloudResourceManagerApiBaseUrl: gcpConfig.cloudResourceManagerApiBaseUrl,
|
|
498
|
+
storageApiBaseUrl: gcpConfig.storageApiBaseUrl,
|
|
499
|
+
fetch: fetchImpl,
|
|
500
|
+
maxRetries: 0,
|
|
501
|
+
});
|
|
502
|
+
stdout(`OK (${result.summary})\n`);
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
stdout(`FAIL (${e instanceof Error ? e.message : String(e)})\n`);
|
|
506
|
+
failed = true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
stdout("Skipping GCP (not in rest mode)\n");
|
|
512
|
+
}
|
|
513
|
+
const docsConfig = loadedConfig.config.adapters.docs;
|
|
514
|
+
if (docsConfig.mode === "local") {
|
|
515
|
+
stdout("Testing local docs roots... ");
|
|
516
|
+
try {
|
|
517
|
+
const result = await smokeLocalDocsRoots({
|
|
518
|
+
workspace: docsConfig.workspace,
|
|
519
|
+
roots: resolveDocsRoots(loadedConfig),
|
|
520
|
+
maxSearchResults: docsConfig.maxSearchResults,
|
|
521
|
+
maxReadBytes: docsConfig.maxReadBytes,
|
|
522
|
+
allowedExtensions: docsConfig.allowedExtensions,
|
|
523
|
+
});
|
|
524
|
+
stdout(`OK (${result.summary})\n`);
|
|
525
|
+
}
|
|
526
|
+
catch (e) {
|
|
527
|
+
stdout(`FAIL (${e instanceof Error ? e.message : String(e)})\n`);
|
|
528
|
+
failed = true;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
stdout("Skipping local docs (not in local mode)\n");
|
|
533
|
+
}
|
|
534
|
+
return failed ? 1 : 0;
|
|
535
|
+
}
|
|
536
|
+
export async function runTestPolicy(input) {
|
|
537
|
+
const loadedConfig = await loadAxtaryConfig({
|
|
538
|
+
filePath: input.configPath,
|
|
539
|
+
cwd: input.cwd,
|
|
540
|
+
});
|
|
541
|
+
const cwd = resolve(input.cwd ?? process.cwd());
|
|
542
|
+
const fixturesPath = resolve(cwd, input.fixturesPath);
|
|
543
|
+
const fixtureFiles = await collectFixtureFiles(fixturesPath);
|
|
544
|
+
const results = [];
|
|
545
|
+
for (const filePath of fixtureFiles) {
|
|
546
|
+
const fixture = await readPolicyFixture(filePath);
|
|
547
|
+
const action = parseNormalizedAction(fixture.action);
|
|
548
|
+
const decision = evaluatePolicy(action, loadedConfig.config.policy);
|
|
549
|
+
const reasonsPassed = !fixture.expectedReasons ||
|
|
550
|
+
arraysEqual(fixture.expectedReasons, decision.reasons);
|
|
551
|
+
const passed = decision.decision === fixture.expectedDecision && reasonsPassed;
|
|
552
|
+
results.push({
|
|
553
|
+
filePath,
|
|
554
|
+
name: fixture.name ?? filePath,
|
|
555
|
+
passed,
|
|
556
|
+
expectedDecision: fixture.expectedDecision,
|
|
557
|
+
actualDecision: decision.decision,
|
|
558
|
+
expectedReasons: fixture.expectedReasons ?? null,
|
|
559
|
+
actualReasons: decision.reasons,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
const failed = results.filter((result) => !result.passed).length;
|
|
563
|
+
return {
|
|
564
|
+
schemaVersion: POLICY_TEST_SCHEMA_VERSION,
|
|
565
|
+
config: {
|
|
566
|
+
filePath: loadedConfig.filePath,
|
|
567
|
+
policyVersion: loadedConfig.config.policy.version,
|
|
568
|
+
},
|
|
569
|
+
fixturesPath,
|
|
570
|
+
passed: failed === 0,
|
|
571
|
+
total: results.length,
|
|
572
|
+
failed,
|
|
573
|
+
results,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
export async function runLedgerExport(input = {}) {
|
|
577
|
+
const loadedConfig = await loadAxtaryConfig({
|
|
578
|
+
filePath: input.configPath,
|
|
579
|
+
cwd: input.cwd,
|
|
580
|
+
});
|
|
581
|
+
const ledgerPath = resolveConfiguredLedgerPath(input, loadedConfig);
|
|
582
|
+
const exported = await exportLedgerRecords({
|
|
583
|
+
filePath: ledgerPath,
|
|
584
|
+
from: input.from,
|
|
585
|
+
to: input.to,
|
|
586
|
+
decisions: input.decisions,
|
|
587
|
+
});
|
|
588
|
+
const format = parseExportFormat(input.format);
|
|
589
|
+
const formatted = formatLedgerExport({
|
|
590
|
+
export: exported,
|
|
591
|
+
format,
|
|
592
|
+
});
|
|
593
|
+
const outputPath = input.outputPath
|
|
594
|
+
? resolve(input.cwd ?? process.cwd(), input.outputPath)
|
|
595
|
+
: null;
|
|
596
|
+
if (outputPath) {
|
|
597
|
+
await writeFile(outputPath, formatted, "utf8");
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
schemaVersion: LEDGER_EXPORT_RUN_SCHEMA_VERSION,
|
|
601
|
+
config: {
|
|
602
|
+
filePath: loadedConfig.filePath,
|
|
603
|
+
tenant: loadedConfig.config.tenant,
|
|
604
|
+
},
|
|
605
|
+
ledgerPath,
|
|
606
|
+
outputPath,
|
|
607
|
+
format,
|
|
608
|
+
export: exported,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
export async function runLedgerSync(input) {
|
|
612
|
+
const loadedConfig = await loadAxtaryConfig({
|
|
613
|
+
filePath: input.configPath,
|
|
614
|
+
cwd: input.cwd,
|
|
615
|
+
});
|
|
616
|
+
const ledgerPath = resolveConfiguredLedgerPath(input, loadedConfig);
|
|
617
|
+
const exported = await exportLedgerRecords({
|
|
618
|
+
filePath: ledgerPath,
|
|
619
|
+
from: input.from,
|
|
620
|
+
to: input.to,
|
|
621
|
+
decisions: input.decisions,
|
|
622
|
+
});
|
|
623
|
+
const env = input.env ?? process.env;
|
|
624
|
+
const token = input.tokenEnv ? env[input.tokenEnv] : undefined;
|
|
625
|
+
if (input.tokenEnv && !token) {
|
|
626
|
+
throw new Error(`missing_ledger_sync_token_env:${input.tokenEnv}`);
|
|
627
|
+
}
|
|
628
|
+
const sync = await syncLedgerExport({
|
|
629
|
+
endpoint: input.endpoint,
|
|
630
|
+
tenant: input.tenant ?? loadedConfig.config.tenant,
|
|
631
|
+
token,
|
|
632
|
+
export: exported,
|
|
633
|
+
fetch: input.fetch,
|
|
634
|
+
});
|
|
635
|
+
return {
|
|
636
|
+
schemaVersion: LEDGER_SYNC_RUN_SCHEMA_VERSION,
|
|
637
|
+
config: {
|
|
638
|
+
filePath: loadedConfig.filePath,
|
|
639
|
+
tenant: loadedConfig.config.tenant,
|
|
640
|
+
},
|
|
641
|
+
ledgerPath,
|
|
642
|
+
endpoint: input.endpoint,
|
|
643
|
+
sync,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
export async function runAxtaryCli(argv = process.argv.slice(2), io = {}) {
|
|
647
|
+
const stdout = io.stdout ?? ((text) => process.stdout.write(text));
|
|
648
|
+
const stderr = io.stderr ?? ((text) => process.stderr.write(text));
|
|
649
|
+
const [command, ...args] = argv;
|
|
650
|
+
try {
|
|
651
|
+
if (!command || command === "--help" || command === "-h") {
|
|
652
|
+
stdout(helpText());
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
if (command === "smoke") {
|
|
656
|
+
const parsed = parseSmokeArgs(args);
|
|
657
|
+
return await runSmokeTest({
|
|
658
|
+
configPath: parsed.configPath,
|
|
659
|
+
cwd: io.cwd ?? parsed.cwd,
|
|
660
|
+
}, io);
|
|
661
|
+
}
|
|
662
|
+
if (command === "demo") {
|
|
663
|
+
const parsed = parseDemoArgs(args);
|
|
664
|
+
const result = await runDemo({
|
|
665
|
+
configPath: parsed.configPath,
|
|
666
|
+
ledgerPath: parsed.ledgerPath,
|
|
667
|
+
cwd: io.cwd ?? parsed.cwd,
|
|
668
|
+
});
|
|
669
|
+
if (parsed.json) {
|
|
670
|
+
stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
stdout(formatDemoResult(result));
|
|
674
|
+
}
|
|
675
|
+
return result.ledger.valid ? 0 : 1;
|
|
676
|
+
}
|
|
677
|
+
if (command === "export-ledger") {
|
|
678
|
+
const parsed = parseLedgerExportArgs(args);
|
|
679
|
+
const result = await runLedgerExport({
|
|
680
|
+
configPath: parsed.configPath,
|
|
681
|
+
ledgerPath: parsed.ledgerPath,
|
|
682
|
+
cwd: io.cwd ?? parsed.cwd,
|
|
683
|
+
from: parsed.from,
|
|
684
|
+
to: parsed.to,
|
|
685
|
+
decisions: parsed.decisions,
|
|
686
|
+
outputPath: parsed.outputPath,
|
|
687
|
+
format: parsed.format,
|
|
688
|
+
});
|
|
689
|
+
if (parsed.json || !parsed.outputPath) {
|
|
690
|
+
stdout(formatLedgerExport({ export: result.export, format: result.format }));
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
stdout(formatLedgerExportResult(result));
|
|
694
|
+
}
|
|
695
|
+
return 0;
|
|
696
|
+
}
|
|
697
|
+
if (command === "sync-ledger") {
|
|
698
|
+
const parsed = parseLedgerSyncArgs(args);
|
|
699
|
+
const result = await runLedgerSync({
|
|
700
|
+
configPath: parsed.configPath,
|
|
701
|
+
ledgerPath: parsed.ledgerPath,
|
|
702
|
+
cwd: io.cwd ?? parsed.cwd,
|
|
703
|
+
from: parsed.from,
|
|
704
|
+
to: parsed.to,
|
|
705
|
+
decisions: parsed.decisions,
|
|
706
|
+
endpoint: parsed.endpoint,
|
|
707
|
+
tenant: parsed.tenant,
|
|
708
|
+
tokenEnv: parsed.tokenEnv,
|
|
709
|
+
});
|
|
710
|
+
if (parsed.json) {
|
|
711
|
+
stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
stdout(formatLedgerSyncResult(result));
|
|
715
|
+
}
|
|
716
|
+
return 0;
|
|
717
|
+
}
|
|
718
|
+
if (command === "proxy") {
|
|
719
|
+
const parsed = parseProxyArgs(args);
|
|
720
|
+
const server = await startProxyServer({
|
|
721
|
+
configPath: parsed.configPath,
|
|
722
|
+
ledgerPath: parsed.ledgerPath,
|
|
723
|
+
cwd: io.cwd ?? parsed.cwd,
|
|
724
|
+
host: parsed.host,
|
|
725
|
+
port: parsed.port,
|
|
726
|
+
});
|
|
727
|
+
stdout(`Axtary proxy listening on ${server.url}\n`);
|
|
728
|
+
stdout(`ledger: ${server.ledgerPath}\n`);
|
|
729
|
+
await waitForShutdown(server.close);
|
|
730
|
+
return 0;
|
|
731
|
+
}
|
|
732
|
+
if (command === "test-policy") {
|
|
733
|
+
const parsed = parseTestPolicyArgs(args);
|
|
734
|
+
const result = await runTestPolicy({
|
|
735
|
+
configPath: parsed.configPath,
|
|
736
|
+
fixturesPath: parsed.fixturesPath,
|
|
737
|
+
cwd: io.cwd ?? parsed.cwd,
|
|
738
|
+
});
|
|
739
|
+
if (parsed.json) {
|
|
740
|
+
stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
stdout(formatPolicyTestResult(result));
|
|
744
|
+
}
|
|
745
|
+
return result.passed ? 0 : 1;
|
|
746
|
+
}
|
|
747
|
+
stderr(`Unknown command: ${command}\n\n${helpText()}`);
|
|
748
|
+
return 1;
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
stderr(`${error instanceof Error ? error.message : "axtary_cli_failed"}\n`);
|
|
752
|
+
return 1;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function parseProxyArgs(args) {
|
|
756
|
+
const parsed = {};
|
|
757
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
758
|
+
const arg = args[index];
|
|
759
|
+
switch (arg) {
|
|
760
|
+
case "--config":
|
|
761
|
+
parsed.configPath = requireValue(args, index, "--config");
|
|
762
|
+
index += 1;
|
|
763
|
+
break;
|
|
764
|
+
case "--ledger":
|
|
765
|
+
parsed.ledgerPath = requireValue(args, index, "--ledger");
|
|
766
|
+
index += 1;
|
|
767
|
+
break;
|
|
768
|
+
case "--cwd":
|
|
769
|
+
parsed.cwd = requireValue(args, index, "--cwd");
|
|
770
|
+
index += 1;
|
|
771
|
+
break;
|
|
772
|
+
case "--host":
|
|
773
|
+
parsed.host = requireValue(args, index, "--host");
|
|
774
|
+
index += 1;
|
|
775
|
+
break;
|
|
776
|
+
case "--port":
|
|
777
|
+
parsed.port = Number.parseInt(requireValue(args, index, "--port"), 10);
|
|
778
|
+
if (!Number.isInteger(parsed.port) || parsed.port < 0) {
|
|
779
|
+
throw new Error("invalid_port");
|
|
780
|
+
}
|
|
781
|
+
index += 1;
|
|
782
|
+
break;
|
|
783
|
+
default:
|
|
784
|
+
throw new Error(`unknown_proxy_flag:${arg}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return parsed;
|
|
788
|
+
}
|
|
789
|
+
function parseTestPolicyArgs(args) {
|
|
790
|
+
const parsed = {
|
|
791
|
+
json: false,
|
|
792
|
+
};
|
|
793
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
794
|
+
const arg = args[index];
|
|
795
|
+
switch (arg) {
|
|
796
|
+
case "--config":
|
|
797
|
+
parsed.configPath = requireValue(args, index, "--config");
|
|
798
|
+
index += 1;
|
|
799
|
+
break;
|
|
800
|
+
case "--fixtures":
|
|
801
|
+
parsed.fixturesPath = requireValue(args, index, "--fixtures");
|
|
802
|
+
index += 1;
|
|
803
|
+
break;
|
|
804
|
+
case "--cwd":
|
|
805
|
+
parsed.cwd = requireValue(args, index, "--cwd");
|
|
806
|
+
index += 1;
|
|
807
|
+
break;
|
|
808
|
+
case "--json":
|
|
809
|
+
parsed.json = true;
|
|
810
|
+
break;
|
|
811
|
+
default:
|
|
812
|
+
throw new Error(`unknown_test_policy_flag:${arg}`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (!parsed.fixturesPath) {
|
|
816
|
+
throw new Error("missing_value:--fixtures");
|
|
817
|
+
}
|
|
818
|
+
return parsed;
|
|
819
|
+
}
|
|
820
|
+
function parseSmokeArgs(args) {
|
|
821
|
+
const parsed = {};
|
|
822
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
823
|
+
const arg = args[index];
|
|
824
|
+
switch (arg) {
|
|
825
|
+
case "--config":
|
|
826
|
+
parsed.configPath = requireValue(args, index, "--config");
|
|
827
|
+
index += 1;
|
|
828
|
+
break;
|
|
829
|
+
case "--cwd":
|
|
830
|
+
parsed.cwd = requireValue(args, index, "--cwd");
|
|
831
|
+
index += 1;
|
|
832
|
+
break;
|
|
833
|
+
default:
|
|
834
|
+
throw new Error(`unknown_smoke_flag:${arg}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return parsed;
|
|
838
|
+
}
|
|
839
|
+
function parseLedgerExportArgs(args) {
|
|
840
|
+
const parsed = {
|
|
841
|
+
json: false,
|
|
842
|
+
decisions: [],
|
|
843
|
+
};
|
|
844
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
845
|
+
const arg = args[index];
|
|
846
|
+
switch (arg) {
|
|
847
|
+
case "--config":
|
|
848
|
+
parsed.configPath = requireValue(args, index, "--config");
|
|
849
|
+
index += 1;
|
|
850
|
+
break;
|
|
851
|
+
case "--ledger":
|
|
852
|
+
parsed.ledgerPath = requireValue(args, index, "--ledger");
|
|
853
|
+
index += 1;
|
|
854
|
+
break;
|
|
855
|
+
case "--cwd":
|
|
856
|
+
parsed.cwd = requireValue(args, index, "--cwd");
|
|
857
|
+
index += 1;
|
|
858
|
+
break;
|
|
859
|
+
case "--from":
|
|
860
|
+
parsed.from = requireValue(args, index, "--from");
|
|
861
|
+
index += 1;
|
|
862
|
+
break;
|
|
863
|
+
case "--to":
|
|
864
|
+
parsed.to = requireValue(args, index, "--to");
|
|
865
|
+
index += 1;
|
|
866
|
+
break;
|
|
867
|
+
case "--decision":
|
|
868
|
+
parsed.decisions.push(parseDecisionFlag(requireValue(args, index, "--decision")));
|
|
869
|
+
index += 1;
|
|
870
|
+
break;
|
|
871
|
+
case "--out":
|
|
872
|
+
parsed.outputPath = requireValue(args, index, "--out");
|
|
873
|
+
index += 1;
|
|
874
|
+
break;
|
|
875
|
+
case "--format":
|
|
876
|
+
parsed.format = parseExportFormat(requireValue(args, index, "--format"));
|
|
877
|
+
index += 1;
|
|
878
|
+
break;
|
|
879
|
+
case "--json":
|
|
880
|
+
parsed.format = "json";
|
|
881
|
+
parsed.json = true;
|
|
882
|
+
break;
|
|
883
|
+
default:
|
|
884
|
+
throw new Error(`unknown_export_ledger_flag:${arg}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return parsed;
|
|
888
|
+
}
|
|
889
|
+
function parseLedgerSyncArgs(args) {
|
|
890
|
+
const parsed = {
|
|
891
|
+
json: false,
|
|
892
|
+
decisions: [],
|
|
893
|
+
};
|
|
894
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
895
|
+
const arg = args[index];
|
|
896
|
+
switch (arg) {
|
|
897
|
+
case "--config":
|
|
898
|
+
parsed.configPath = requireValue(args, index, "--config");
|
|
899
|
+
index += 1;
|
|
900
|
+
break;
|
|
901
|
+
case "--ledger":
|
|
902
|
+
parsed.ledgerPath = requireValue(args, index, "--ledger");
|
|
903
|
+
index += 1;
|
|
904
|
+
break;
|
|
905
|
+
case "--cwd":
|
|
906
|
+
parsed.cwd = requireValue(args, index, "--cwd");
|
|
907
|
+
index += 1;
|
|
908
|
+
break;
|
|
909
|
+
case "--from":
|
|
910
|
+
parsed.from = requireValue(args, index, "--from");
|
|
911
|
+
index += 1;
|
|
912
|
+
break;
|
|
913
|
+
case "--to":
|
|
914
|
+
parsed.to = requireValue(args, index, "--to");
|
|
915
|
+
index += 1;
|
|
916
|
+
break;
|
|
917
|
+
case "--decision":
|
|
918
|
+
parsed.decisions.push(parseDecisionFlag(requireValue(args, index, "--decision")));
|
|
919
|
+
index += 1;
|
|
920
|
+
break;
|
|
921
|
+
case "--endpoint":
|
|
922
|
+
parsed.endpoint = requireValue(args, index, "--endpoint");
|
|
923
|
+
index += 1;
|
|
924
|
+
break;
|
|
925
|
+
case "--tenant":
|
|
926
|
+
parsed.tenant = requireValue(args, index, "--tenant");
|
|
927
|
+
index += 1;
|
|
928
|
+
break;
|
|
929
|
+
case "--token-env":
|
|
930
|
+
parsed.tokenEnv = requireValue(args, index, "--token-env");
|
|
931
|
+
index += 1;
|
|
932
|
+
break;
|
|
933
|
+
case "--json":
|
|
934
|
+
parsed.json = true;
|
|
935
|
+
break;
|
|
936
|
+
default:
|
|
937
|
+
throw new Error(`unknown_sync_ledger_flag:${arg}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (!parsed.endpoint) {
|
|
941
|
+
throw new Error("missing_value:--endpoint");
|
|
942
|
+
}
|
|
943
|
+
return parsed;
|
|
944
|
+
}
|
|
945
|
+
function parseDemoArgs(args) {
|
|
946
|
+
const parsed = {
|
|
947
|
+
json: false,
|
|
948
|
+
};
|
|
949
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
950
|
+
const arg = args[index];
|
|
951
|
+
switch (arg) {
|
|
952
|
+
case "--config":
|
|
953
|
+
parsed.configPath = requireValue(args, index, "--config");
|
|
954
|
+
index += 1;
|
|
955
|
+
break;
|
|
956
|
+
case "--ledger":
|
|
957
|
+
parsed.ledgerPath = requireValue(args, index, "--ledger");
|
|
958
|
+
index += 1;
|
|
959
|
+
break;
|
|
960
|
+
case "--cwd":
|
|
961
|
+
parsed.cwd = requireValue(args, index, "--cwd");
|
|
962
|
+
index += 1;
|
|
963
|
+
break;
|
|
964
|
+
case "--json":
|
|
965
|
+
parsed.json = true;
|
|
966
|
+
break;
|
|
967
|
+
default:
|
|
968
|
+
throw new Error(`unknown_demo_flag:${arg}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return parsed;
|
|
972
|
+
}
|
|
973
|
+
function toDemoActionResult(name, handleResult) {
|
|
974
|
+
if (handleResult.status === "malformed") {
|
|
975
|
+
return {
|
|
976
|
+
name,
|
|
977
|
+
tool: "unknown",
|
|
978
|
+
status: handleResult.status,
|
|
979
|
+
decision: null,
|
|
980
|
+
reasons: [handleResult.error.reason],
|
|
981
|
+
actionPassId: null,
|
|
982
|
+
ledgerLine: null,
|
|
983
|
+
result: null,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
name,
|
|
988
|
+
tool: handleResult.action.capability.tool,
|
|
989
|
+
status: handleResult.status,
|
|
990
|
+
decision: handleResult.decision.decision,
|
|
991
|
+
reasons: handleResult.decision.reasons,
|
|
992
|
+
actionPassId: handleResult.actionPass?.claims.jti ?? null,
|
|
993
|
+
ledgerLine: handleResult.ledger.lineNumber,
|
|
994
|
+
result: handleResult.status === "executed" ? handleResult.result : null,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function ledgerSummary(verifiedLedger) {
|
|
998
|
+
if (verifiedLedger.valid) {
|
|
999
|
+
return {
|
|
1000
|
+
valid: true,
|
|
1001
|
+
records: verifiedLedger.records.length,
|
|
1002
|
+
lastLedgerHash: verifiedLedger.lastLedgerHash,
|
|
1003
|
+
failure: null,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
valid: false,
|
|
1008
|
+
records: verifiedLedger.records.length,
|
|
1009
|
+
lastLedgerHash: verifiedLedger.records.at(-1)?.ledgerHash ?? null,
|
|
1010
|
+
failure: `${verifiedLedger.lineNumber}:${verifiedLedger.reason}`,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function formatDemoResult(result) {
|
|
1014
|
+
const lines = [
|
|
1015
|
+
"Axtary demo complete",
|
|
1016
|
+
`ledger: ${result.ledgerPath}`,
|
|
1017
|
+
`ledger valid: ${result.ledger.valid}`,
|
|
1018
|
+
"",
|
|
1019
|
+
];
|
|
1020
|
+
for (const entry of result.results) {
|
|
1021
|
+
lines.push(`${entry.name}: ${entry.status} ${entry.decision ?? "none"} (${entry.reasons.join(", ")})`);
|
|
1022
|
+
}
|
|
1023
|
+
lines.push("");
|
|
1024
|
+
lines.push(`fake GitHub PRs: ${result.adapters.github.pullRequests.length}, fake Slack messages: ${result.adapters.slack.messages.length}, fake Linear comments: ${result.adapters.issueTrackers.linear.comments.length}, fake Jira comments: ${result.adapters.issueTrackers.jira.comments.length}`);
|
|
1025
|
+
return `${lines.join("\n")}\n`;
|
|
1026
|
+
}
|
|
1027
|
+
function formatPolicyTestResult(result) {
|
|
1028
|
+
const lines = [
|
|
1029
|
+
`Axtary policy fixtures ${result.passed ? "passed" : "failed"}`,
|
|
1030
|
+
`fixtures: ${result.fixturesPath}`,
|
|
1031
|
+
`total: ${result.total}, failed: ${result.failed}`,
|
|
1032
|
+
"",
|
|
1033
|
+
];
|
|
1034
|
+
for (const entry of result.results) {
|
|
1035
|
+
lines.push(`${entry.passed ? "PASS" : "FAIL"} ${entry.name}: expected ${entry.expectedDecision}, got ${entry.actualDecision} (${entry.actualReasons.join(", ")})`);
|
|
1036
|
+
}
|
|
1037
|
+
return `${lines.join("\n")}\n`;
|
|
1038
|
+
}
|
|
1039
|
+
function formatLedgerExportResult(result) {
|
|
1040
|
+
const lines = [
|
|
1041
|
+
"Axtary ledger export complete",
|
|
1042
|
+
`ledger: ${result.ledgerPath}`,
|
|
1043
|
+
`records: ${result.export.records.length} of ${result.export.source.totalRecords}`,
|
|
1044
|
+
`last hash: ${result.export.source.lastLedgerHash ?? "none"}`,
|
|
1045
|
+
];
|
|
1046
|
+
if (result.outputPath) {
|
|
1047
|
+
lines.push(`output: ${result.outputPath}`);
|
|
1048
|
+
}
|
|
1049
|
+
return `${lines.join("\n")}\n`;
|
|
1050
|
+
}
|
|
1051
|
+
function formatLedgerSyncResult(result) {
|
|
1052
|
+
return [
|
|
1053
|
+
"Axtary ledger sync complete",
|
|
1054
|
+
`endpoint: ${result.endpoint}`,
|
|
1055
|
+
`status: ${result.sync.status}`,
|
|
1056
|
+
`records: ${result.sync.records}`,
|
|
1057
|
+
`last hash: ${result.sync.lastLedgerHash ?? "none"}`,
|
|
1058
|
+
"",
|
|
1059
|
+
].join("\n");
|
|
1060
|
+
}
|
|
1061
|
+
function resolveConfiguredLedgerPath(input, loadedConfig) {
|
|
1062
|
+
const cwd = resolve(input.cwd ?? process.cwd());
|
|
1063
|
+
return input.ledgerPath ? resolve(cwd, input.ledgerPath) : loadedConfig.ledgerPath;
|
|
1064
|
+
}
|
|
1065
|
+
function resolveDocsRoots(loadedConfig) {
|
|
1066
|
+
return loadedConfig.config.adapters.docs.roots.map((root) => resolve(loadedConfig.baseDir, root));
|
|
1067
|
+
}
|
|
1068
|
+
function parseDecisionFlag(value) {
|
|
1069
|
+
return AxtaryDecisionSchema.parse(value);
|
|
1070
|
+
}
|
|
1071
|
+
function parseExportFormat(value) {
|
|
1072
|
+
return LedgerExportFormatSchema.parse(value ?? "json");
|
|
1073
|
+
}
|
|
1074
|
+
function createProxyHandlers(loadedConfig, adapterState, verification) {
|
|
1075
|
+
const fakeHandlers = createFakeHandlers(adapterState, { verification });
|
|
1076
|
+
const githubConfig = loadedConfig.config.adapters.github;
|
|
1077
|
+
const slackConfig = loadedConfig.config.adapters.slack;
|
|
1078
|
+
const linearConfig = loadedConfig.config.adapters.linear;
|
|
1079
|
+
const awsConfig = loadedConfig.config.adapters.aws;
|
|
1080
|
+
const gcpConfig = loadedConfig.config.adapters.gcp;
|
|
1081
|
+
const docsConfig = loadedConfig.config.adapters.docs;
|
|
1082
|
+
const handlers = { ...fakeHandlers };
|
|
1083
|
+
const adapterModes = {
|
|
1084
|
+
github: githubConfig.mode,
|
|
1085
|
+
slack: slackConfig.mode,
|
|
1086
|
+
linear: linearConfig.mode,
|
|
1087
|
+
aws: awsConfig.mode,
|
|
1088
|
+
gcp: gcpConfig.mode,
|
|
1089
|
+
docs: docsConfig.mode,
|
|
1090
|
+
mcp: "fixture",
|
|
1091
|
+
};
|
|
1092
|
+
if (githubConfig.mode === "rest") {
|
|
1093
|
+
const token = process.env[githubConfig.tokenEnv];
|
|
1094
|
+
if (!token) {
|
|
1095
|
+
throw new Error(`missing_github_token_env:${githubConfig.tokenEnv}`);
|
|
1096
|
+
}
|
|
1097
|
+
Object.assign(handlers, createGitHubRestHandlers({
|
|
1098
|
+
token,
|
|
1099
|
+
apiBaseUrl: githubConfig.apiBaseUrl,
|
|
1100
|
+
userAgent: githubConfig.userAgent,
|
|
1101
|
+
}, { verification }));
|
|
1102
|
+
}
|
|
1103
|
+
if (slackConfig.mode === "web") {
|
|
1104
|
+
const token = process.env[slackConfig.tokenEnv];
|
|
1105
|
+
if (!token) {
|
|
1106
|
+
throw new Error(`missing_slack_token_env:${slackConfig.tokenEnv}`);
|
|
1107
|
+
}
|
|
1108
|
+
Object.assign(handlers, createSlackWebHandlers({
|
|
1109
|
+
token,
|
|
1110
|
+
apiBaseUrl: slackConfig.apiBaseUrl,
|
|
1111
|
+
}, { verification }));
|
|
1112
|
+
}
|
|
1113
|
+
if (linearConfig.mode === "graphql") {
|
|
1114
|
+
const token = process.env[linearConfig.tokenEnv];
|
|
1115
|
+
if (!token) {
|
|
1116
|
+
throw new Error(`missing_linear_token_env:${linearConfig.tokenEnv}`);
|
|
1117
|
+
}
|
|
1118
|
+
Object.assign(handlers, createLinearGraphqlHandlers({
|
|
1119
|
+
token,
|
|
1120
|
+
apiUrl: linearConfig.apiUrl,
|
|
1121
|
+
}, { verification }));
|
|
1122
|
+
}
|
|
1123
|
+
if (awsConfig.mode === "rest") {
|
|
1124
|
+
const accessKeyId = process.env[awsConfig.accessKeyIdEnv];
|
|
1125
|
+
const secretAccessKey = process.env[awsConfig.secretAccessKeyEnv];
|
|
1126
|
+
const sessionToken = process.env[awsConfig.sessionTokenEnv];
|
|
1127
|
+
if (!accessKeyId) {
|
|
1128
|
+
throw new Error(`missing_aws_access_key_id_env:${awsConfig.accessKeyIdEnv}`);
|
|
1129
|
+
}
|
|
1130
|
+
if (!secretAccessKey) {
|
|
1131
|
+
throw new Error(`missing_aws_secret_access_key_env:${awsConfig.secretAccessKeyEnv}`);
|
|
1132
|
+
}
|
|
1133
|
+
Object.assign(handlers, createAwsRestHandlers({
|
|
1134
|
+
credentials: {
|
|
1135
|
+
accessKeyId,
|
|
1136
|
+
secretAccessKey,
|
|
1137
|
+
sessionToken,
|
|
1138
|
+
},
|
|
1139
|
+
region: awsConfig.region,
|
|
1140
|
+
stsUrl: awsConfig.stsUrl,
|
|
1141
|
+
}, { verification }));
|
|
1142
|
+
}
|
|
1143
|
+
if (gcpConfig.mode === "rest") {
|
|
1144
|
+
const accessToken = process.env[gcpConfig.accessTokenEnv];
|
|
1145
|
+
if (!accessToken) {
|
|
1146
|
+
throw new Error(`missing_gcp_access_token_env:${gcpConfig.accessTokenEnv}`);
|
|
1147
|
+
}
|
|
1148
|
+
Object.assign(handlers, createGcpRestHandlers({
|
|
1149
|
+
accessToken,
|
|
1150
|
+
projectId: gcpConfig.projectId,
|
|
1151
|
+
cloudResourceManagerApiBaseUrl: gcpConfig.cloudResourceManagerApiBaseUrl,
|
|
1152
|
+
storageApiBaseUrl: gcpConfig.storageApiBaseUrl,
|
|
1153
|
+
}, { verification }));
|
|
1154
|
+
}
|
|
1155
|
+
if (docsConfig.mode === "local") {
|
|
1156
|
+
Object.assign(handlers, createLocalDocsHandlers({
|
|
1157
|
+
workspace: docsConfig.workspace,
|
|
1158
|
+
roots: resolveDocsRoots(loadedConfig),
|
|
1159
|
+
maxSearchResults: docsConfig.maxSearchResults,
|
|
1160
|
+
maxReadBytes: docsConfig.maxReadBytes,
|
|
1161
|
+
allowedExtensions: docsConfig.allowedExtensions,
|
|
1162
|
+
}, { verification }));
|
|
1163
|
+
}
|
|
1164
|
+
return {
|
|
1165
|
+
handlers,
|
|
1166
|
+
adapterModes,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function helpText() {
|
|
1170
|
+
return [
|
|
1171
|
+
"Usage:",
|
|
1172
|
+
" axtary demo [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--json]",
|
|
1173
|
+
" 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
|
+
" 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
|
+
" axtary proxy [--config axtary.yml] [--ledger .axtary/actions.jsonl] [--host 127.0.0.1] [--port 7331]",
|
|
1176
|
+
" axtary smoke [--config axtary.yml]",
|
|
1177
|
+
" axtary test-policy --fixtures ./fixtures [--config axtary.yml] [--json]",
|
|
1178
|
+
"",
|
|
1179
|
+
].join("\n");
|
|
1180
|
+
}
|
|
1181
|
+
function requireValue(args, index, flag) {
|
|
1182
|
+
const value = args[index + 1];
|
|
1183
|
+
if (!value || value.startsWith("--")) {
|
|
1184
|
+
throw new Error(`missing_value:${flag}`);
|
|
1185
|
+
}
|
|
1186
|
+
return value;
|
|
1187
|
+
}
|
|
1188
|
+
async function collectFixtureFiles(path) {
|
|
1189
|
+
const pathStat = await stat(path);
|
|
1190
|
+
if (pathStat.isFile()) {
|
|
1191
|
+
return [path];
|
|
1192
|
+
}
|
|
1193
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
1194
|
+
const files = [];
|
|
1195
|
+
for (const entry of entries) {
|
|
1196
|
+
const entryPath = resolve(path, entry.name);
|
|
1197
|
+
if (entry.isDirectory()) {
|
|
1198
|
+
files.push(...(await collectFixtureFiles(entryPath)));
|
|
1199
|
+
}
|
|
1200
|
+
else if (entry.isFile() && extname(entry.name) === ".json") {
|
|
1201
|
+
files.push(entryPath);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return files.sort();
|
|
1205
|
+
}
|
|
1206
|
+
async function readPolicyFixture(filePath) {
|
|
1207
|
+
const text = await readFile(filePath, "utf8");
|
|
1208
|
+
const parsed = JSON.parse(text);
|
|
1209
|
+
if (!parsed.action) {
|
|
1210
|
+
throw new Error(`fixture_missing_action:${filePath}`);
|
|
1211
|
+
}
|
|
1212
|
+
if (parsed.expectedDecision !== "allow" &&
|
|
1213
|
+
parsed.expectedDecision !== "deny" &&
|
|
1214
|
+
parsed.expectedDecision !== "step_up") {
|
|
1215
|
+
throw new Error(`fixture_invalid_expected_decision:${filePath}`);
|
|
1216
|
+
}
|
|
1217
|
+
if (parsed.expectedReasons &&
|
|
1218
|
+
!parsed.expectedReasons.every((reason) => typeof reason === "string")) {
|
|
1219
|
+
throw new Error(`fixture_invalid_expected_reasons:${filePath}`);
|
|
1220
|
+
}
|
|
1221
|
+
return {
|
|
1222
|
+
name: typeof parsed.name === "string" ? parsed.name : undefined,
|
|
1223
|
+
action: parsed.action,
|
|
1224
|
+
expectedDecision: parsed.expectedDecision,
|
|
1225
|
+
expectedReasons: parsed.expectedReasons,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
function arraysEqual(left, right) {
|
|
1229
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1230
|
+
}
|
|
1231
|
+
function listen(server, port, host) {
|
|
1232
|
+
return new Promise((resolveListen, reject) => {
|
|
1233
|
+
server.once("error", reject);
|
|
1234
|
+
server.listen(port, host, () => {
|
|
1235
|
+
server.off("error", reject);
|
|
1236
|
+
resolveListen();
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
function close(server) {
|
|
1241
|
+
return new Promise((resolveClose, reject) => {
|
|
1242
|
+
server.close((error) => {
|
|
1243
|
+
if (error) {
|
|
1244
|
+
reject(error);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
resolveClose();
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
function writeJson(response, statusCode, value) {
|
|
1252
|
+
response.writeHead(statusCode, {
|
|
1253
|
+
"content-type": "application/json; charset=utf-8",
|
|
1254
|
+
});
|
|
1255
|
+
response.end(`${JSON.stringify(value, null, 2)}\n`);
|
|
1256
|
+
}
|
|
1257
|
+
async function readJsonBody(request) {
|
|
1258
|
+
const chunks = [];
|
|
1259
|
+
for await (const chunk of request) {
|
|
1260
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
1261
|
+
}
|
|
1262
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1263
|
+
}
|
|
1264
|
+
function waitForShutdown(closeServer) {
|
|
1265
|
+
return new Promise((resolveWait) => {
|
|
1266
|
+
const shutdown = () => {
|
|
1267
|
+
process.off("SIGINT", shutdown);
|
|
1268
|
+
process.off("SIGTERM", shutdown);
|
|
1269
|
+
void closeServer().finally(resolveWait);
|
|
1270
|
+
};
|
|
1271
|
+
process.once("SIGINT", shutdown);
|
|
1272
|
+
process.once("SIGTERM", shutdown);
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
//# sourceMappingURL=index.js.map
|