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.
Files changed (3) hide show
  1. package/README.md +15 -1
  2. package/bin/totem.mjs +573 -64
  3. 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 --help
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.0";
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 help() {
34
- process.stdout.write(`TotemLayer CLI ${CLI_VERSION}
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
- Install:
37
- npm install --global totemlayer
38
- npx --yes totemlayer --help
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
- Usage:
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. The RFQ command requires a
80
- sandbox or self-hosted Totem deployment with the procurement demo enabled.
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(`Totem: ${terminalText(message, "The Totem command failed.")}\n`);
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({ baseUrl, path, key, body, idempotencyKey, timeout = 30_000 }) {
455
- const headers = {
456
- authorization: `Bearer ${key}`,
457
- "content-type": "application/json",
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: "POST",
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
- process.stdout.write(`Totem challenge · ${terminalText(challenge.id)}\n`);
556
- process.stdout.write(`Action: ${terminalText(challenge.requestedAction ?? requestedAction)}\n`);
557
- process.stdout.write(`Expires: ${terminalText(challenge.expiresAt)}\n`);
558
- process.stdout.write(
559
- "Use --json to emit the nonce-bearing challenge for the presenting agent.\n",
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
- process.stdout.write("Totem presentation · REJECTED\n");
597
- process.stdout.write(`Reason: ${apiError.code}\n`);
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
- process.stdout.write(`Totem presentation · ${terminalText(presentation.challengeId)}\n`);
621
- process.stdout.write(`Credential: ${terminalText(presentation.credentialId)}\n`);
622
- process.stdout.write(`Expires: ${terminalText(presentation.expiresAt)}\n`);
623
- process.stdout.write("Use --json to emit the token-bearing presentation for the verifier.\n");
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
- process.stdout.write("Totem verification · REJECTED\n");
678
- process.stdout.write(`Reason: ${apiError.code}\n`);
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
- process.stdout.write(`Totem verification · ${terminalText(payload.result)}\n`);
744
- if (payload.agent?.name || payload.agent?.id) {
745
- process.stdout.write(`Agent: ${terminalText(payload.agent.name ?? payload.agent.id)}\n`);
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
- if (payload.organization?.name || payload.organization?.id) {
748
- process.stdout.write(
749
- `Organization: ${terminalText(payload.organization.name ?? payload.organization.id)}\n`,
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
- if (payload.authorization?.requestedAction) {
753
- process.stdout.write(`Action: ${terminalText(payload.authorization.requestedAction)}\n`);
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
- if (payload.reasonCode) process.stdout.write(`Reason: ${terminalText(payload.reasonCode)}\n`);
756
- if (payload.requestId) process.stdout.write(`Receipt: ${terminalText(payload.requestId)}\n`);
1160
+ writeJson({ suite });
1161
+ return;
757
1162
  }
758
- if (payload.verified && payload.result === "APPROVED" && result.response.ok) return;
759
- process.exitCode = payload.result === "REJECTED" ? 2 : 1;
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
- process.stdout.write(`Totem RFQ · ${terminalText(run.id, "unknown run")}\n`);
817
- process.stdout.write(`Run: ${terminalText(run.status, "UNKNOWN")}\n`);
818
- if (run.summary) process.stdout.write(`${terminalText(run.summary)}\n`);
819
- if (verification)
820
- process.stdout.write(`Agent authority: ${terminalText(verification.result, "UNKNOWN")}\n`);
821
- if (supplier) {
822
- process.stdout.write(`Delivery: ${terminalText(supplier.status, "UNKNOWN")}\n`);
823
- process.stdout.write(
824
- `Supplier response: ${terminalText(supplier.businessStatus, "UNKNOWN")}\n`,
825
- );
826
- process.stdout.write(
827
- `Available now: ${number(supplier.availableQuantity)} ${terminalText(supplier.unit, "units")}\n`,
828
- );
829
- process.stdout.write(
830
- `Backorder: ${number(supplier.backorderQuantity)} ${terminalText(supplier.unit, "units")}\n`,
831
- );
832
- if (supplier.supplierReference)
833
- process.stdout.write(`Reference: ${terminalText(supplier.supplierReference)}\n`);
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 && command !== "rfq") failCommand(message, options?.json);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totemlayer",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Cross-platform CLI for TotemLayer agent identity and authorization",
5
5
  "type": "module",
6
6
  "bin": {