totemlayer 0.1.0 → 0.1.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 +15 -1
- package/bin/totem.mjs +573 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,21 @@ Node.js 22.14 or newer is required.
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install --global totemlayer
|
|
13
|
-
totem
|
|
13
|
+
totem
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Running `totem` opens the branded command guide, confirms the installed version,
|
|
17
|
+
and shows the complete challenge → presentation → verification flow. Interactive
|
|
18
|
+
commands return a readable result card with the decision, relevant evidence, and
|
|
19
|
+
the safest next step. Add `--json` when the output is being saved or consumed by
|
|
20
|
+
another program; machine-readable output remains undecorated JSON.
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
╭╮
|
|
24
|
+
╭───╯╰────╮ ╺┳╸┏━┓╺┳╸┏━╸┏┳┓
|
|
25
|
+
╰────╮╭────╯ ┃ ┃ ┃ ┃ ┣╸ ┃┃┃
|
|
26
|
+
╰╯ ╹ ┗━┛ ╹ ┗━╸╹ ╹
|
|
27
|
+
TotemLayer CLI 0.1.1 · cross-company agent authorization
|
|
14
28
|
```
|
|
15
29
|
|
|
16
30
|
For a one-off run without a global install:
|
package/bin/totem.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
6
6
|
|
|
7
7
|
const RFQ_EXAMPLE =
|
|
8
8
|
"Request a quote for 50,000 Industrial Sensors (SKU-1001) from SupplyCo using Acme Procurement Agent.";
|
|
9
|
-
const CLI_VERSION = "0.1.
|
|
9
|
+
const CLI_VERSION = "0.1.1";
|
|
10
10
|
const DEFAULT_BASE_URL = "https://totemlayer.com";
|
|
11
11
|
const API_KEY_PATTERN = /^at_(?:live|test)_[A-Za-z0-9_-]{20,}$/;
|
|
12
12
|
const ACTION_PATTERN = /^[a-z0-9][a-z0-9._:-]*$/;
|
|
@@ -16,6 +16,12 @@ const TERMINAL_UNSAFE_PATTERN =
|
|
|
16
16
|
/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
|
|
17
17
|
const MAX_JSON_INPUT_BYTES = 128 * 1_024;
|
|
18
18
|
const MAX_REQUEST_FILE_BYTES = 16 * 1_024 * 1_024;
|
|
19
|
+
const BRAND_ART = [
|
|
20
|
+
" ╭╮",
|
|
21
|
+
" ╭───╯╰────╮ ╺┳╸┏━┓╺┳╸┏━╸┏┳┓",
|
|
22
|
+
" ╰────╮╭────╯ ┃ ┃ ┃ ┃ ┣╸ ┃┃┃",
|
|
23
|
+
" ╰╯ ╹ ┗━┛ ╹ ┗━╸╹ ╹",
|
|
24
|
+
];
|
|
19
25
|
const PRESENTATION_REJECTION_CODES = new Set([
|
|
20
26
|
"CHALLENGE_NOT_FOUND",
|
|
21
27
|
"CHALLENGE_NOT_PRESENTED",
|
|
@@ -29,20 +35,91 @@ const PRESENTATION_REJECTION_CODES = new Set([
|
|
|
29
35
|
"INVALID_PRESENTATION_SIGNATURE",
|
|
30
36
|
"PRESENTATION_CLAIMS_MISMATCH",
|
|
31
37
|
]);
|
|
38
|
+
const ACTION_CHECK_POLICY_CODES = new Set([
|
|
39
|
+
"ACTION_APPROVAL_REQUIRED",
|
|
40
|
+
"ACTION_APPROVAL_STALE",
|
|
41
|
+
"ACTION_AUTHORITY_CHANGED",
|
|
42
|
+
"ACTION_CHECKS_NOT_ALLOWED",
|
|
43
|
+
"ACTION_CONFIRMATION_MISMATCH",
|
|
44
|
+
"ACTION_DIGEST_MISMATCH",
|
|
45
|
+
"ACTION_EXECUTION_ATTEMPT_CONFLICT",
|
|
46
|
+
"ACTION_EXPIRED",
|
|
47
|
+
"ACTION_FINAL_CHECKS_FAILED",
|
|
48
|
+
"ACTION_GRANT_BINDING_MISMATCH",
|
|
49
|
+
"ACTION_GRANT_EXPIRED",
|
|
50
|
+
"ACTION_GRANT_INVALID",
|
|
51
|
+
"ACTION_GRANT_REPLAYED",
|
|
52
|
+
"ACTION_POLICY_CHANGED",
|
|
53
|
+
"ACTION_RECEIPT_STATE_CHANGED",
|
|
54
|
+
"ACTION_SUITE_EXPIRED",
|
|
55
|
+
"ACTION_SUITE_INCOMPLETE",
|
|
56
|
+
"ACTION_SUITE_STALE",
|
|
57
|
+
"ACTION_SUITE_STATE_CHANGED",
|
|
58
|
+
"DEMO_FIXTURE_INACTIVE",
|
|
59
|
+
"DEMO_FIXTURE_NOT_OWNED",
|
|
60
|
+
"DESTRUCTIVE_TARGET_UNCONSTRAINED",
|
|
61
|
+
"EXECUTOR_ADAPTER_MISMATCH",
|
|
62
|
+
"EXECUTOR_ADAPTER_NOT_PUBLIC",
|
|
63
|
+
"FINANCIAL_CONTEXT_MISSING",
|
|
64
|
+
]);
|
|
32
65
|
|
|
33
|
-
function
|
|
34
|
-
process.
|
|
66
|
+
function supportsColor(stream = process.stdout) {
|
|
67
|
+
return Boolean(stream.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function styled(value, code, stream = process.stdout) {
|
|
71
|
+
return supportsColor(stream) ? `\u001b[${code}m${value}\u001b[0m` : value;
|
|
72
|
+
}
|
|
35
73
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
function writeBrand() {
|
|
75
|
+
const art = BRAND_ART.map((line, index) =>
|
|
76
|
+
styled(line, index < 2 ? "38;2;58;153;200" : "38;2;21;37;131"),
|
|
77
|
+
).join("\n");
|
|
78
|
+
process.stdout.write(`${art}\n`);
|
|
79
|
+
process.stdout.write(
|
|
80
|
+
`${styled(`TotemLayer CLI ${CLI_VERSION}`, "1")} · cross-company agent authorization\n\n`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeHumanResult(header, fields, nextStep, tone = "success") {
|
|
85
|
+
const toneCode = tone === "danger" ? "38;2;255;114;114" : "38;2;58;153;200";
|
|
86
|
+
process.stdout.write(`${styled(header, `1;${toneCode}`)}\n`);
|
|
87
|
+
process.stdout.write(`${styled("────────────────────────────────────────", "2")}\n`);
|
|
88
|
+
for (const [label, value] of fields) {
|
|
89
|
+
if (value === undefined || value === null || value === "") continue;
|
|
90
|
+
process.stdout.write(`${styled(`${label}:`, "2")} ${terminalText(value)}\n`);
|
|
91
|
+
}
|
|
92
|
+
if (nextStep) process.stdout.write(`\n${styled("Next →", "1")} ${nextStep}\n`);
|
|
93
|
+
}
|
|
39
94
|
|
|
40
|
-
|
|
95
|
+
function compactDigest(value) {
|
|
96
|
+
const digest = terminalText(value, "not returned");
|
|
97
|
+
return digest.length > 28 ? `${digest.slice(0, 16)}…${digest.slice(-8)}` : digest;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function help() {
|
|
101
|
+
writeBrand();
|
|
102
|
+
process.stdout.write(`Start here
|
|
103
|
+
Install once npm install --global totemlayer
|
|
104
|
+
Confirm installation totem --version
|
|
105
|
+
Run one-off npx --yes totemlayer --help
|
|
106
|
+
|
|
107
|
+
Authorization handshake
|
|
41
108
|
totem challenge --presenter-agent <agent-id> --action <action> \\
|
|
42
109
|
(--request-digest <sha256> | --request-file <path>) [--amount <number> --currency <ISO-4217>] --json
|
|
43
110
|
totem present --credential-id <credential-id> [--challenge-file <path>] --json
|
|
44
111
|
totem verify --challenge-file <receiver-challenge.json> \\
|
|
45
112
|
(--request-digest <sha256> | --request-file <path>) [--presentation-file <path>] --json
|
|
113
|
+
|
|
114
|
+
Action Checks · local or self-hosted
|
|
115
|
+
totem checks run --file <action.json>
|
|
116
|
+
totem checks view <suite-id>
|
|
117
|
+
totem checks approve <suite-id> [--file <decision.json>]
|
|
118
|
+
totem checks reject <suite-id> [--file <decision.json>]
|
|
119
|
+
totem checks grant <suite-id> --file <action.json>
|
|
120
|
+
totem checks execute <suite-id> --file <execution.json>
|
|
121
|
+
|
|
122
|
+
Procurement demonstration
|
|
46
123
|
totem rfq "${RFQ_EXAMPLE}" # sandbox/self-hosted demo only
|
|
47
124
|
|
|
48
125
|
Shared options:
|
|
@@ -71,18 +148,38 @@ Presentation input:
|
|
|
71
148
|
compares it with the receiver-owned --challenge-file before consuming the proof.
|
|
72
149
|
Raw keys, nonces, credentials, and presentation tokens are never accepted as arguments.
|
|
73
150
|
|
|
151
|
+
Action Checks input (local/self-hosted only):
|
|
152
|
+
--file <path> run/grant: exact action JSON; execute: {"action":...,"grant":"..."}
|
|
153
|
+
decision.json Optional {"confirmation":"...","reason":"..."}; never put the
|
|
154
|
+
confirmation, grant, session cookie, or action payload on the command line
|
|
155
|
+
Action Checks always emit structured JSON. A blocked, rejected, expired, stale, or replayed
|
|
156
|
+
action exits 2; invalid input, network, authentication, and operational failures exit 1.
|
|
157
|
+
|
|
74
158
|
Authentication:
|
|
75
159
|
challenge and verify use TOTEM_API_KEY.
|
|
76
160
|
present uses TOTEM_AGENT_API_KEY bound to the presenting agent.
|
|
161
|
+
checks run/view/grant/execute use TOTEM_API_KEY with an Action Checks scope.
|
|
162
|
+
checks approve/reject never use an API key. They require an authenticated human browser
|
|
163
|
+
session supplied only through TOTEM_SESSION_COOKIE as a Cookie header value. Clear it after
|
|
164
|
+
the command. Without it, use the /checks browser page.
|
|
77
165
|
Keys are sent only in the Authorization header and are never printed.
|
|
78
166
|
|
|
79
|
-
Production supports challenge, present, and verify.
|
|
80
|
-
sandbox or self-hosted Totem deployment with
|
|
167
|
+
Production supports challenge, present, and verify. Action Checks and RFQ commands require a
|
|
168
|
+
sandbox or self-hosted Totem deployment with their respective local demonstrations enabled.
|
|
169
|
+
|
|
170
|
+
Human-readable commands show the decision, relevant evidence, and the safest next step.
|
|
171
|
+
Add --json whenever another program needs the complete machine-readable response.
|
|
81
172
|
`);
|
|
82
173
|
}
|
|
83
174
|
|
|
84
175
|
function fail(message) {
|
|
85
|
-
process.stderr.write(
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
`${styled("Totem: command failed", "1;38;2;255;114;114", process.stderr)}\n`,
|
|
178
|
+
);
|
|
179
|
+
process.stderr.write(`Reason: ${terminalText(message, "The Totem command failed.")}\n`);
|
|
180
|
+
process.stderr.write(
|
|
181
|
+
"Next: Fix the reason above, or run `totem --help` for a complete example.\n",
|
|
182
|
+
);
|
|
86
183
|
process.exitCode = 1;
|
|
87
184
|
}
|
|
88
185
|
|
|
@@ -118,6 +215,7 @@ function parseArguments(values) {
|
|
|
118
215
|
"--challenge-file",
|
|
119
216
|
"--credential-id",
|
|
120
217
|
"--presentation-file",
|
|
218
|
+
"--file",
|
|
121
219
|
]);
|
|
122
220
|
for (let index = 0; index < values.length; index += 1) {
|
|
123
221
|
const value = values[index];
|
|
@@ -162,6 +260,14 @@ function safeBaseUrl(value) {
|
|
|
162
260
|
return url;
|
|
163
261
|
}
|
|
164
262
|
|
|
263
|
+
function requireLocalActionChecks(baseUrl) {
|
|
264
|
+
if (baseUrl.origin === DEFAULT_BASE_URL) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"Action Checks commands are available only on a local or self-hosted Totem deployment. Set TOTEM_URL to that deployment; use the /checks browser page for human decisions.",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
165
271
|
function aliasedEnvironment(primaryName, legacyName) {
|
|
166
272
|
const primary = process.env[primaryName]?.trim();
|
|
167
273
|
const legacy = process.env[legacyName]?.trim();
|
|
@@ -181,6 +287,45 @@ function apiKey(environmentName, legacyEnvironmentName) {
|
|
|
181
287
|
return value;
|
|
182
288
|
}
|
|
183
289
|
|
|
290
|
+
function actionCheckSessionCookie() {
|
|
291
|
+
const raw = process.env.TOTEM_SESSION_COOKIE;
|
|
292
|
+
if (!raw?.trim()) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
"Action check decisions require an authenticated human browser session. Open the local /checks page, or set TOTEM_SESSION_COOKIE from your own current browser session. API keys cannot approve or reject.",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const value = raw.trim();
|
|
298
|
+
if (value.length > 16 * 1_024 || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
299
|
+
throw new Error("TOTEM_SESSION_COOKIE is not a valid browser Cookie header.");
|
|
300
|
+
}
|
|
301
|
+
const pairs = value
|
|
302
|
+
.split(";")
|
|
303
|
+
.map((pair) => pair.trim())
|
|
304
|
+
.filter(Boolean);
|
|
305
|
+
let hasSessionCookie = false;
|
|
306
|
+
for (const pair of pairs) {
|
|
307
|
+
const separator = pair.indexOf("=");
|
|
308
|
+
const name = separator > 0 ? pair.slice(0, separator) : "";
|
|
309
|
+
const cookieValue = separator > 0 ? pair.slice(separator + 1) : "";
|
|
310
|
+
if (
|
|
311
|
+
!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name) ||
|
|
312
|
+
!cookieValue ||
|
|
313
|
+
/[\u0000-\u0020\u007f;]/.test(cookieValue)
|
|
314
|
+
) {
|
|
315
|
+
throw new Error("TOTEM_SESSION_COOKIE is not a valid browser Cookie header.");
|
|
316
|
+
}
|
|
317
|
+
if (/^(?:__Secure-)?(?:next-auth|authjs)\.session-token(?:\.\d+)?$/.test(name)) {
|
|
318
|
+
hasSessionCookie = true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!hasSessionCookie) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
"TOTEM_SESSION_COOKIE does not contain a Totem browser session. Open the local /checks page to approve or reject instead; API keys cannot make human decisions.",
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
|
|
184
329
|
function errorFrom(payload, fallback) {
|
|
185
330
|
if (!payload || typeof payload !== "object") return fallback;
|
|
186
331
|
const error = payload.error;
|
|
@@ -190,6 +335,45 @@ function errorFrom(payload, fallback) {
|
|
|
190
335
|
return `${code}: ${message}`;
|
|
191
336
|
}
|
|
192
337
|
|
|
338
|
+
function redactCliSecrets(value, secrets) {
|
|
339
|
+
let redacted = terminalText(value, "The Action Checks request failed.");
|
|
340
|
+
for (const secret of secrets) {
|
|
341
|
+
if (typeof secret === "string" && secret.length >= 8)
|
|
342
|
+
redacted = redacted.replaceAll(secret, "[REDACTED]");
|
|
343
|
+
}
|
|
344
|
+
return redacted;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function writeActionCheckFailure(payload, fallback, status, secrets = []) {
|
|
348
|
+
const error = record(record(payload)?.error);
|
|
349
|
+
const code = redactCliSecrets(
|
|
350
|
+
typeof error?.code === "string" ? error.code : "ACTION_CHECK_REQUEST_FAILED",
|
|
351
|
+
secrets,
|
|
352
|
+
);
|
|
353
|
+
const message = redactCliSecrets(
|
|
354
|
+
typeof error?.message === "string" ? error.message : fallback,
|
|
355
|
+
secrets,
|
|
356
|
+
);
|
|
357
|
+
const safePayload = { error: { code, message } };
|
|
358
|
+
writeJson(safePayload);
|
|
359
|
+
const policyRejection =
|
|
360
|
+
status < 500 &&
|
|
361
|
+
(ACTION_CHECK_POLICY_CODES.has(code) ||
|
|
362
|
+
(status === 409 && code !== "ACTION_APPROVAL_NOT_REQUIRED"));
|
|
363
|
+
process.exitCode = policyRejection ? 2 : 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function writeActionCheckOperationalError(message, outcome) {
|
|
367
|
+
writeJson({
|
|
368
|
+
error: {
|
|
369
|
+
code: "ACTION_CHECK_OPERATIONAL_ERROR",
|
|
370
|
+
message: terminalText(message, "The Action Checks command failed."),
|
|
371
|
+
...(outcome ? { outcome } : {}),
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
process.exitCode = 1;
|
|
375
|
+
}
|
|
376
|
+
|
|
193
377
|
function number(value) {
|
|
194
378
|
return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US") : "—";
|
|
195
379
|
}
|
|
@@ -237,6 +421,7 @@ function validateCommandOptions(command, options) {
|
|
|
237
421
|
"request_digest",
|
|
238
422
|
"request_file",
|
|
239
423
|
]),
|
|
424
|
+
checks: new Set([...shared, "file"]),
|
|
240
425
|
}[command];
|
|
241
426
|
if (!allowed) throw new Error(`Unknown command ${command || "(missing)"}.`);
|
|
242
427
|
for (const [key, value] of Object.entries(options)) {
|
|
@@ -451,18 +636,28 @@ function boundPresentationClaims(token, challenge, credentialId, expiresAt) {
|
|
|
451
636
|
return claims;
|
|
452
637
|
}
|
|
453
638
|
|
|
454
|
-
async function requestJson({
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
639
|
+
async function requestJson({
|
|
640
|
+
baseUrl,
|
|
641
|
+
path,
|
|
642
|
+
key,
|
|
643
|
+
cookie,
|
|
644
|
+
method = "POST",
|
|
645
|
+
body,
|
|
646
|
+
idempotencyKey,
|
|
647
|
+
timeout = 30_000,
|
|
648
|
+
}) {
|
|
649
|
+
const headers = { accept: "application/json" };
|
|
650
|
+
if (key) headers.authorization = `Bearer ${key}`;
|
|
651
|
+
if (cookie) headers.cookie = cookie;
|
|
652
|
+
if (body !== undefined) headers["content-type"] = "application/json";
|
|
459
653
|
if (idempotencyKey) headers["idempotency-key"] = idempotencyKey;
|
|
460
654
|
let response;
|
|
461
655
|
try {
|
|
462
656
|
response = await fetch(new URL(path, baseUrl), {
|
|
463
|
-
method
|
|
657
|
+
method,
|
|
464
658
|
headers,
|
|
465
|
-
body: JSON.stringify(body),
|
|
659
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
660
|
+
redirect: "manual",
|
|
466
661
|
signal: AbortSignal.timeout(timeout),
|
|
467
662
|
});
|
|
468
663
|
} catch (error) {
|
|
@@ -552,11 +747,24 @@ async function runChallenge(options, baseUrl) {
|
|
|
552
747
|
}
|
|
553
748
|
if (options.json) writeJson(result.payload);
|
|
554
749
|
else {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
750
|
+
writeHumanResult(
|
|
751
|
+
`Totem challenge · ${terminalText(challenge.id)}`,
|
|
752
|
+
[
|
|
753
|
+
[
|
|
754
|
+
"Status",
|
|
755
|
+
result.payload?.replayed ? "READY · existing idempotent result" : "READY · created",
|
|
756
|
+
],
|
|
757
|
+
["Presenter", challenge.presenterAgentId ?? presenterAgentId],
|
|
758
|
+
["Action", challenge.requestedAction ?? requestedAction],
|
|
759
|
+
["Request", compactDigest(challenge.requestDigest ?? requestDigest)],
|
|
760
|
+
[
|
|
761
|
+
"Transaction",
|
|
762
|
+
transactionAmount !== undefined ? `${number(transactionAmount)} ${currency}` : undefined,
|
|
763
|
+
],
|
|
764
|
+
["Expires", challenge.expiresAt],
|
|
765
|
+
["Protection", "Recipient-bound · request-bound · single-use"],
|
|
766
|
+
],
|
|
767
|
+
"Run again with --json to save the nonce-bearing challenge for the presenting agent.",
|
|
560
768
|
);
|
|
561
769
|
}
|
|
562
770
|
}
|
|
@@ -593,8 +801,16 @@ async function runPresent(options, baseUrl) {
|
|
|
593
801
|
if (typeof apiError?.code === "string" && PRESENTATION_REJECTION_CODES.has(apiError.code)) {
|
|
594
802
|
if (options.json) writeJson(result.payload);
|
|
595
803
|
else {
|
|
596
|
-
|
|
597
|
-
|
|
804
|
+
writeHumanResult(
|
|
805
|
+
"Totem presentation · REJECTED",
|
|
806
|
+
[
|
|
807
|
+
["Status", "Proof was not created"],
|
|
808
|
+
["Challenge", challengeId],
|
|
809
|
+
["Reason", apiError.code],
|
|
810
|
+
],
|
|
811
|
+
"Return to the receiver for a fresh challenge before trying again.",
|
|
812
|
+
"danger",
|
|
813
|
+
);
|
|
598
814
|
}
|
|
599
815
|
process.exitCode = 2;
|
|
600
816
|
return;
|
|
@@ -617,10 +833,19 @@ async function runPresent(options, baseUrl) {
|
|
|
617
833
|
}
|
|
618
834
|
if (options.json) writeJson(result.payload);
|
|
619
835
|
else {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
836
|
+
writeHumanResult(
|
|
837
|
+
`Totem presentation · ${terminalText(presentation.challengeId)}`,
|
|
838
|
+
[
|
|
839
|
+
["Status", "READY · signed proof created"],
|
|
840
|
+
["Challenge", presentation.challengeId],
|
|
841
|
+
["Credential", presentation.credentialId],
|
|
842
|
+
["Presenter", challenge.presenterAgentId],
|
|
843
|
+
["Action", challenge.requestedAction],
|
|
844
|
+
["Expires", presentation.expiresAt],
|
|
845
|
+
["Protection", "Challenge-bound · recipient-bound · one-minute proof"],
|
|
846
|
+
],
|
|
847
|
+
"Run again with --json to save the token-bearing presentation for the verifier.",
|
|
848
|
+
);
|
|
624
849
|
}
|
|
625
850
|
}
|
|
626
851
|
|
|
@@ -674,8 +899,17 @@ async function runVerify(options, baseUrl) {
|
|
|
674
899
|
if (typeof apiError?.code === "string" && PRESENTATION_REJECTION_CODES.has(apiError.code)) {
|
|
675
900
|
if (options.json) writeJson(payload);
|
|
676
901
|
else {
|
|
677
|
-
|
|
678
|
-
|
|
902
|
+
writeHumanResult(
|
|
903
|
+
"Totem verification · REJECTED",
|
|
904
|
+
[
|
|
905
|
+
["Decision", "DO NOT ACT"],
|
|
906
|
+
["Challenge", challenge.id],
|
|
907
|
+
["Action", challenge.requestedAction],
|
|
908
|
+
["Reason", apiError.code],
|
|
909
|
+
],
|
|
910
|
+
"Create a fresh challenge and verify the exact request again before taking action.",
|
|
911
|
+
"danger",
|
|
912
|
+
);
|
|
679
913
|
}
|
|
680
914
|
process.exitCode = 2;
|
|
681
915
|
return;
|
|
@@ -740,23 +974,285 @@ async function runVerify(options, baseUrl) {
|
|
|
740
974
|
|
|
741
975
|
if (options.json) writeJson(payload);
|
|
742
976
|
else {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
977
|
+
const approvedResult = payload.verified && payload.result === "APPROVED" && result.response.ok;
|
|
978
|
+
writeHumanResult(
|
|
979
|
+
`Totem verification · ${terminalText(payload.result)}`,
|
|
980
|
+
[
|
|
981
|
+
["Decision", approvedResult ? "AUTHORIZED FOR THIS EXACT REQUEST" : "DO NOT ACT"],
|
|
982
|
+
["Agent", payload.agent?.name ?? payload.agent?.id],
|
|
983
|
+
["Agent ID", payload.agent?.id],
|
|
984
|
+
["Organization", payload.organization?.name ?? payload.organization?.id],
|
|
985
|
+
[
|
|
986
|
+
"Organization proof",
|
|
987
|
+
payload.organization?.verified
|
|
988
|
+
? `${terminalText(payload.organization.domain, "verified domain")} · verified`
|
|
989
|
+
: payload.organization?.domain,
|
|
990
|
+
],
|
|
991
|
+
["Action", payload.authorization?.requestedAction],
|
|
992
|
+
[
|
|
993
|
+
"Authority",
|
|
994
|
+
payload.authorization?.allowed ? "Active credential permits this action" : undefined,
|
|
995
|
+
],
|
|
996
|
+
[
|
|
997
|
+
"Human review",
|
|
998
|
+
typeof payload.authorization?.humanApprovalRequired === "boolean"
|
|
999
|
+
? payload.authorization.humanApprovalRequired
|
|
1000
|
+
? "Required"
|
|
1001
|
+
: "Not required by credential"
|
|
1002
|
+
: undefined,
|
|
1003
|
+
],
|
|
1004
|
+
["Credential", payload.evidence?.credentialId],
|
|
1005
|
+
["Request", compactDigest(payload.evidence?.requestDigest ?? expectedRequestDigest)],
|
|
1006
|
+
["Verified", payload.evidence?.verifiedAt],
|
|
1007
|
+
["Reason", payload.reasonCode],
|
|
1008
|
+
["Receipt", payload.requestId],
|
|
1009
|
+
],
|
|
1010
|
+
approvedResult
|
|
1011
|
+
? "Continue only with the unchanged request bytes shown by this approval."
|
|
1012
|
+
: "Do not process the request; resolve the reason and create a fresh proof.",
|
|
1013
|
+
approvedResult ? "success" : "danger",
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
if (payload.verified && payload.result === "APPROVED" && result.response.ok) return;
|
|
1017
|
+
process.exitCode = payload.result === "REJECTED" ? 2 : 1;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function checksFile(options, command) {
|
|
1021
|
+
const path = options.file?.trim();
|
|
1022
|
+
if (!path || path.length > 4_096 || path.includes("\0")) {
|
|
1023
|
+
throw new Error(`${command} requires --file with a readable JSON file.`);
|
|
1024
|
+
}
|
|
1025
|
+
return path;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function checksSuiteId(options, subcommand) {
|
|
1029
|
+
if (options.positionals.length !== 3) {
|
|
1030
|
+
throw new Error(`checks ${subcommand} requires exactly one suite ID.`);
|
|
1031
|
+
}
|
|
1032
|
+
const suiteId = options.positionals[2];
|
|
1033
|
+
if (
|
|
1034
|
+
typeof suiteId !== "string" ||
|
|
1035
|
+
suiteId.length > 200 ||
|
|
1036
|
+
!SAFE_IDENTIFIER_PATTERN.test(suiteId)
|
|
1037
|
+
) {
|
|
1038
|
+
throw new Error("The Action Checks suite ID is invalid.");
|
|
1039
|
+
}
|
|
1040
|
+
return suiteId;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function decisionFile(options) {
|
|
1044
|
+
if (!options.file) return { confirmation: null, reason: null };
|
|
1045
|
+
const input = await readJson(checksFile(options, "The decision command"), "decision JSON");
|
|
1046
|
+
if (Object.keys(input).some((key) => !["confirmation", "reason"].includes(key))) {
|
|
1047
|
+
throw new Error("Decision JSON may contain only confirmation and reason.");
|
|
1048
|
+
}
|
|
1049
|
+
const confirmation = input.confirmation;
|
|
1050
|
+
const reason = input.reason;
|
|
1051
|
+
if (
|
|
1052
|
+
confirmation !== undefined &&
|
|
1053
|
+
confirmation !== null &&
|
|
1054
|
+
(typeof confirmation !== "string" || confirmation.length > 500)
|
|
1055
|
+
) {
|
|
1056
|
+
throw new Error("Decision confirmation must be a string of at most 500 characters.");
|
|
1057
|
+
}
|
|
1058
|
+
if (
|
|
1059
|
+
reason !== undefined &&
|
|
1060
|
+
reason !== null &&
|
|
1061
|
+
(typeof reason !== "string" || reason.length > 1_000)
|
|
1062
|
+
) {
|
|
1063
|
+
throw new Error("Decision reason must be a string of at most 1000 characters.");
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
confirmation:
|
|
1067
|
+
typeof confirmation === "string" && confirmation.trim() ? confirmation.trim() : null,
|
|
1068
|
+
reason: typeof reason === "string" && reason.trim() ? reason.trim() : null,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async function actionCheckRequest({
|
|
1073
|
+
baseUrl,
|
|
1074
|
+
path,
|
|
1075
|
+
method = "POST",
|
|
1076
|
+
key,
|
|
1077
|
+
cookie,
|
|
1078
|
+
body,
|
|
1079
|
+
secrets = [],
|
|
1080
|
+
}) {
|
|
1081
|
+
let result;
|
|
1082
|
+
try {
|
|
1083
|
+
result = await requestJson({ baseUrl, path, method, key, cookie, body });
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
writeActionCheckOperationalError(
|
|
1086
|
+
error instanceof Error ? error.message : "Could not reach Totem.",
|
|
1087
|
+
method === "GET" ? undefined : "unknown",
|
|
1088
|
+
);
|
|
1089
|
+
return undefined;
|
|
1090
|
+
}
|
|
1091
|
+
if (!result.response.ok) {
|
|
1092
|
+
writeActionCheckFailure(
|
|
1093
|
+
result.payload,
|
|
1094
|
+
`The Action Checks endpoint returned HTTP ${result.response.status}.`,
|
|
1095
|
+
result.response.status,
|
|
1096
|
+
secrets,
|
|
1097
|
+
);
|
|
1098
|
+
return undefined;
|
|
1099
|
+
}
|
|
1100
|
+
const payload = record(result.payload);
|
|
1101
|
+
if (!payload) {
|
|
1102
|
+
writeActionCheckOperationalError("Totem returned an invalid Action Checks response.");
|
|
1103
|
+
return undefined;
|
|
1104
|
+
}
|
|
1105
|
+
return payload;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async function runChecks(options, baseUrl) {
|
|
1109
|
+
requireLocalActionChecks(baseUrl);
|
|
1110
|
+
const subcommand = options.positionals[1];
|
|
1111
|
+
if (!subcommand)
|
|
1112
|
+
throw new Error(
|
|
1113
|
+
"Provide an Action Checks command: run, view, approve, reject, grant, or execute.",
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
if (subcommand === "run") {
|
|
1117
|
+
if (options.positionals.length !== 2)
|
|
1118
|
+
throw new Error("checks run does not accept positional values.");
|
|
1119
|
+
const action = await readJson(checksFile(options, "checks run"), "action JSON");
|
|
1120
|
+
const key = apiKey("TOTEM_API_KEY", "AGENTTRUST_API_KEY");
|
|
1121
|
+
const payload = await actionCheckRequest({
|
|
1122
|
+
baseUrl,
|
|
1123
|
+
path: "/api/action-checks",
|
|
1124
|
+
key,
|
|
1125
|
+
body: { action },
|
|
1126
|
+
secrets: [key],
|
|
1127
|
+
});
|
|
1128
|
+
if (!payload) return;
|
|
1129
|
+
const suite = record(payload.suite);
|
|
1130
|
+
if (!suite || typeof suite.id !== "string" || typeof suite.conclusion !== "string") {
|
|
1131
|
+
writeActionCheckOperationalError("Totem returned an invalid action check suite.");
|
|
1132
|
+
return;
|
|
746
1133
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1134
|
+
writeJson({ suite });
|
|
1135
|
+
if (suite.status === "STALE" || ["BLOCK", "REQUIRES_APPROVAL"].includes(suite.conclusion)) {
|
|
1136
|
+
process.exitCode = 2;
|
|
1137
|
+
} else if (suite.conclusion === "ERROR") {
|
|
1138
|
+
process.exitCode = 1;
|
|
751
1139
|
}
|
|
752
|
-
|
|
753
|
-
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (subcommand === "view") {
|
|
1144
|
+
if (options.file !== undefined) throw new Error("--file is not valid for checks view.");
|
|
1145
|
+
const suiteId = checksSuiteId(options, subcommand);
|
|
1146
|
+
const key = apiKey("TOTEM_API_KEY", "AGENTTRUST_API_KEY");
|
|
1147
|
+
const payload = await actionCheckRequest({
|
|
1148
|
+
baseUrl,
|
|
1149
|
+
path: `/api/action-checks/${encodeURIComponent(suiteId)}`,
|
|
1150
|
+
method: "GET",
|
|
1151
|
+
key,
|
|
1152
|
+
secrets: [key],
|
|
1153
|
+
});
|
|
1154
|
+
if (!payload) return;
|
|
1155
|
+
const suite = record(payload.suite);
|
|
1156
|
+
if (!suite || typeof suite.id !== "string") {
|
|
1157
|
+
writeActionCheckOperationalError("Totem returned an invalid action check suite.");
|
|
1158
|
+
return;
|
|
754
1159
|
}
|
|
755
|
-
|
|
756
|
-
|
|
1160
|
+
writeJson({ suite });
|
|
1161
|
+
return;
|
|
757
1162
|
}
|
|
758
|
-
|
|
759
|
-
|
|
1163
|
+
|
|
1164
|
+
if (subcommand === "approve" || subcommand === "reject") {
|
|
1165
|
+
const suiteId = checksSuiteId(options, subcommand);
|
|
1166
|
+
const cookie = actionCheckSessionCookie();
|
|
1167
|
+
const decision = subcommand === "approve" ? "APPROVE" : "REJECT";
|
|
1168
|
+
const input = await decisionFile(options);
|
|
1169
|
+
const payload = await actionCheckRequest({
|
|
1170
|
+
baseUrl,
|
|
1171
|
+
path: `/api/action-checks/${encodeURIComponent(suiteId)}/decision`,
|
|
1172
|
+
cookie,
|
|
1173
|
+
body: { decision, confirmation: input.confirmation, reason: input.reason },
|
|
1174
|
+
secrets: [cookie],
|
|
1175
|
+
});
|
|
1176
|
+
if (!payload) return;
|
|
1177
|
+
const suite = record(payload.suite);
|
|
1178
|
+
if (!suite || typeof suite.id !== "string") {
|
|
1179
|
+
writeActionCheckOperationalError("Totem returned an invalid action check decision response.");
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
writeJson({ suite });
|
|
1183
|
+
if (decision === "REJECT") process.exitCode = 2;
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (subcommand === "grant") {
|
|
1188
|
+
const suiteId = checksSuiteId(options, subcommand);
|
|
1189
|
+
const action = await readJson(checksFile(options, "checks grant"), "action JSON");
|
|
1190
|
+
const key = apiKey("TOTEM_API_KEY", "AGENTTRUST_API_KEY");
|
|
1191
|
+
const payload = await actionCheckRequest({
|
|
1192
|
+
baseUrl,
|
|
1193
|
+
path: `/api/action-checks/${encodeURIComponent(suiteId)}/grant`,
|
|
1194
|
+
key,
|
|
1195
|
+
body: { action },
|
|
1196
|
+
secrets: [key],
|
|
1197
|
+
});
|
|
1198
|
+
if (!payload) return;
|
|
1199
|
+
const grant = record(payload.grant);
|
|
1200
|
+
const suite = record(grant?.suite);
|
|
1201
|
+
if (
|
|
1202
|
+
!grant ||
|
|
1203
|
+
typeof grant.token !== "string" ||
|
|
1204
|
+
grant.token.length < 32 ||
|
|
1205
|
+
grant.token.length > 512 ||
|
|
1206
|
+
typeof grant.expiresAt !== "string" ||
|
|
1207
|
+
!suite
|
|
1208
|
+
) {
|
|
1209
|
+
writeActionCheckOperationalError("Totem returned an invalid execution grant response.");
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
writeJson({ grant: { token: grant.token, expiresAt: grant.expiresAt, suite } });
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (subcommand === "execute") {
|
|
1217
|
+
const suiteId = checksSuiteId(options, subcommand);
|
|
1218
|
+
const input = await readJson(checksFile(options, "checks execute"), "execution JSON");
|
|
1219
|
+
const action = record(input.action);
|
|
1220
|
+
const grant = input.grant;
|
|
1221
|
+
if (!action || typeof grant !== "string" || grant.length < 32 || grant.length > 512) {
|
|
1222
|
+
throw new Error("Execution JSON must contain the exact action object and opaque grant.");
|
|
1223
|
+
}
|
|
1224
|
+
const key = apiKey("TOTEM_API_KEY", "AGENTTRUST_API_KEY");
|
|
1225
|
+
const payload = await actionCheckRequest({
|
|
1226
|
+
baseUrl,
|
|
1227
|
+
path: `/api/action-checks/${encodeURIComponent(suiteId)}/execute`,
|
|
1228
|
+
key,
|
|
1229
|
+
body: { action, grant },
|
|
1230
|
+
secrets: [key, grant],
|
|
1231
|
+
});
|
|
1232
|
+
if (!payload) return;
|
|
1233
|
+
const suite = record(payload.suite);
|
|
1234
|
+
const receipt = record(payload.receipt);
|
|
1235
|
+
if (
|
|
1236
|
+
!suite ||
|
|
1237
|
+
!receipt ||
|
|
1238
|
+
typeof receipt.id !== "string" ||
|
|
1239
|
+
typeof receipt.outcome !== "string"
|
|
1240
|
+
) {
|
|
1241
|
+
writeActionCheckOperationalError("Totem returned an invalid execution receipt response.");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
writeJson({
|
|
1245
|
+
suite,
|
|
1246
|
+
receipt,
|
|
1247
|
+
...(typeof payload.receiptId === "string" ? { receiptId: payload.receiptId } : {}),
|
|
1248
|
+
});
|
|
1249
|
+
if (["FAILED", "UNKNOWN", "COMPENSATED"].includes(receipt.outcome)) process.exitCode = 1;
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
"Unknown Action Checks command. Use run, view, approve, reject, grant, or execute.",
|
|
1255
|
+
);
|
|
760
1256
|
}
|
|
761
1257
|
|
|
762
1258
|
async function runRfq(options, baseUrl) {
|
|
@@ -813,25 +1309,36 @@ async function runRfq(options, baseUrl) {
|
|
|
813
1309
|
|
|
814
1310
|
const verification = record(run.verification);
|
|
815
1311
|
const supplier = record(run.supplierRequest);
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1312
|
+
const succeeded = run.status === "SUCCEEDED";
|
|
1313
|
+
writeHumanResult(
|
|
1314
|
+
`Totem RFQ · ${terminalText(run.id, "unknown run")}`,
|
|
1315
|
+
[
|
|
1316
|
+
["Run", run.status ?? "UNKNOWN"],
|
|
1317
|
+
["Request", prompt],
|
|
1318
|
+
["Summary", run.summary],
|
|
1319
|
+
["Agent authority", verification?.result ?? "UNKNOWN"],
|
|
1320
|
+
["Delivery", supplier?.status ?? "UNKNOWN"],
|
|
1321
|
+
["Supplier response", supplier?.businessStatus ?? "UNKNOWN"],
|
|
1322
|
+
[
|
|
1323
|
+
"Available now",
|
|
1324
|
+
supplier
|
|
1325
|
+
? `${number(supplier.availableQuantity)} ${terminalText(supplier.unit, "units")}`
|
|
1326
|
+
: undefined,
|
|
1327
|
+
],
|
|
1328
|
+
[
|
|
1329
|
+
"Backorder",
|
|
1330
|
+
supplier
|
|
1331
|
+
? `${number(supplier.backorderQuantity)} ${terminalText(supplier.unit, "units")}`
|
|
1332
|
+
: undefined,
|
|
1333
|
+
],
|
|
1334
|
+
["Reference", supplier?.supplierReference],
|
|
1335
|
+
["Mode", "Non-binding procurement demonstration"],
|
|
1336
|
+
],
|
|
1337
|
+
succeeded
|
|
1338
|
+
? "Review the supplier response and retain the run ID as the audit reference."
|
|
1339
|
+
: "Open the Totem operator run for the bounded trace before retrying.",
|
|
1340
|
+
succeeded ? "success" : "danger",
|
|
1341
|
+
);
|
|
835
1342
|
if (run.status !== "SUCCEEDED") process.exitCode = 1;
|
|
836
1343
|
}
|
|
837
1344
|
|
|
@@ -855,11 +1362,13 @@ async function main() {
|
|
|
855
1362
|
if (command === "challenge") await runChallenge(options, baseUrl);
|
|
856
1363
|
else if (command === "present") await runPresent(options, baseUrl);
|
|
857
1364
|
else if (command === "verify") await runVerify(options, baseUrl);
|
|
1365
|
+
else if (command === "checks") await runChecks(options, baseUrl);
|
|
858
1366
|
else await runRfq(options, baseUrl);
|
|
859
1367
|
} catch (error) {
|
|
860
1368
|
const message = error instanceof Error ? error.message : "The Totem command failed.";
|
|
861
|
-
const command = options?.positionals?.[0];
|
|
862
|
-
if (command
|
|
1369
|
+
const command = options?.positionals?.[0] ?? process.argv[2];
|
|
1370
|
+
if (command === "checks") writeActionCheckOperationalError(message);
|
|
1371
|
+
else if (command && command !== "rfq") failCommand(message, options?.json);
|
|
863
1372
|
else fail(message);
|
|
864
1373
|
}
|
|
865
1374
|
}
|