@zq-silk/yui 1.1.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/ARCHITECTURE.md +2 -1
  2. package/ARCHITECTURE.zh-CN.md +2 -1
  3. package/README.md +2 -0
  4. package/dist/cli/commandCatalog.js +344 -324
  5. package/dist/cli/commandDiscovery.js +69 -0
  6. package/dist/cli/completion.js +22 -227
  7. package/dist/cli/dynamicCompletion.js +12 -6
  8. package/dist/cli/invocationAuthority.js +2 -2
  9. package/dist/cli/invocationRouter.js +18 -7
  10. package/dist/cli/managedDiagnostics.js +2 -1
  11. package/dist/cli/updatePorts.js +0 -5
  12. package/dist/cli.js +184 -130
  13. package/dist/commands/capabilityCommands.js +5 -5
  14. package/dist/commands/controllerCommands.js +2 -1
  15. package/dist/commands/globalRoleCommands.js +86 -44
  16. package/dist/commands/grantCommands.js +48 -8
  17. package/dist/commands/taskCommands.js +157 -196
  18. package/dist/commands/taskContextCommand.js +29 -6
  19. package/dist/commands/taskFactCommands.js +10 -69
  20. package/dist/commands/taskInputCommands.js +26 -35
  21. package/dist/commands/taskPublicationCommands.js +2 -35
  22. package/dist/commands/taskPublicationVerifyCommand.js +1 -2
  23. package/dist/commands/taskUpstreamCommands.js +2 -1
  24. package/dist/context/runContextPack.js +8 -29
  25. package/dist/context/sessionBootstrapManifest.js +5 -101
  26. package/dist/context/taskContext.js +104 -41
  27. package/dist/controller/clientRuntime.js +16 -8
  28. package/dist/controller/fileSchedulerStoreAdapter.js +24 -0
  29. package/dist/controller/runtime.js +6 -0
  30. package/dist/core/controllerClient.js +46 -21
  31. package/dist/core/protocol.js +12 -7
  32. package/dist/doctor/doctor.js +4 -3
  33. package/dist/errors/cliError.js +4 -4
  34. package/dist/errors/cliFailure.js +213 -0
  35. package/dist/executor/fileRoleLaunchPlanner.js +1 -4
  36. package/dist/grant/capabilityGrant.js +13 -0
  37. package/dist/grant/taskAuthorization.js +127 -0
  38. package/dist/kernel/accessAssessment.js +1 -0
  39. package/dist/kernel/builtinCapabilities.js +88 -9
  40. package/dist/kernel/capabilityRegistry.js +64 -19
  41. package/dist/kernel/kernelPorts.js +2 -2
  42. package/dist/nativeAgent/agent.js +173 -0
  43. package/dist/nativeAgent/contracts.js +1 -0
  44. package/dist/nativeAgent/demo.js +29 -0
  45. package/dist/nativeAgent/index.js +3 -0
  46. package/dist/nativeAgent/mockProvider.js +43 -0
  47. package/dist/nativeAgent/textTools.js +156 -0
  48. package/dist/nativeAgent/validation.js +95 -0
  49. package/dist/output/boundedRead.js +118 -0
  50. package/dist/plugins/pluginService.js +98 -8
  51. package/dist/release/releaseWorkflowEngine.js +8 -1
  52. package/dist/resources/projectResourceService.js +23 -1
  53. package/dist/runtime/managedCaller.js +2 -2
  54. package/dist/runtime/managedIdentity.js +6 -0
  55. package/dist/runtime/runtimeCoherence.js +10 -8
  56. package/dist/storage/sqliteSchema.js +10 -0
  57. package/dist/storage/storageVersions.js +1 -1
  58. package/dist/storage/taskStore.js +3 -0
  59. package/dist/surface/surfaceContributions.js +3 -0
  60. package/dist/task/leaderArchive.js +111 -0
  61. package/dist/task/leaderArchiveAuthority.js +24 -0
  62. package/dist/task/taskAuthority.js +5 -10
  63. package/dist/tmux/commandExecutor.js +7 -5
  64. package/dist/web/assets/assetManifest.js +38 -22
  65. package/dist/web/assets/client/api.js +115 -0
  66. package/dist/web/assets/client/app.js +555 -863
  67. package/dist/web/assets/client/components.js +323 -897
  68. package/dist/web/assets/client/detail.js +333 -0
  69. package/dist/web/assets/client/dock.js +225 -0
  70. package/dist/web/assets/client/dom.js +55 -3
  71. package/dist/web/assets/client/evidence.js +172 -0
  72. package/dist/web/assets/client/format.js +25 -14
  73. package/dist/web/assets/client/forms.js +230 -0
  74. package/dist/web/assets/client/i18n.js +1020 -791
  75. package/dist/web/assets/client/overview.js +115 -0
  76. package/dist/web/assets/client/records.js +63 -0
  77. package/dist/web/assets/client/sections.js +313 -0
  78. package/dist/web/assets/client/sidebar.js +117 -0
  79. package/dist/web/assets/client/theme.js +61 -22
  80. package/dist/web/assets/icons.js +39 -0
  81. package/dist/web/assets/shell.js +124 -118
  82. package/dist/web/assets/styles/base.js +41 -0
  83. package/dist/web/assets/styles/components.js +167 -0
  84. package/dist/web/assets/styles/layout.js +49 -77
  85. package/dist/web/assets/styles/markdown.js +16 -24
  86. package/dist/web/assets/styles/responsive.js +33 -44
  87. package/dist/web/assets/styles/tokens.js +65 -89
  88. package/dist/web/assets/styles/views.js +289 -0
  89. package/dist/web/webServer.js +2 -1
  90. package/docs/architecture/README.md +1 -0
  91. package/docs/architecture/README.zh-CN.md +1 -0
  92. package/docs/cli-information-contract.md +107 -0
  93. package/docs/cli-information-contract.zh-CN.md +81 -0
  94. package/docs/managed-turn-and-session-runtime.md +9 -0
  95. package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
  96. package/docs/plugin-sdk.md +7 -3
  97. package/docs/plugin-sdk.zh-CN.md +5 -2
  98. package/docs/release-workflow.md +42 -9
  99. package/docs/release-workflow.zh-CN.md +30 -7
  100. package/docs/storage-baseline.md +5 -0
  101. package/docs/testing/verification-levels.md +2 -2
  102. package/i18n/README.zh-CN.md +2 -0
  103. package/package.json +1 -1
  104. package/skills/yui-leader/SKILL.md +8 -1
  105. package/skills/yui-leader/references/authorization.md +64 -0
  106. package/skills/yui-leader/references/execution.md +18 -4
  107. package/skills/yui-leader/references/task-plugins.md +4 -2
  108. package/skills/yui-operator/SKILL.md +9 -2
  109. package/skills/yui-runtime/SKILL.md +77 -9
  110. package/skills/yui-runtime/references/publication.md +3 -1
  111. package/dist/web/assets/client/taskSummary.js +0 -350
  112. package/dist/web/assets/client/taskSurface.js +0 -616
  113. package/dist/web/assets/client/view.js +0 -608
  114. package/dist/web/assets/styles/cards.js +0 -247
  115. package/dist/web/assets/styles/widgets.js +0 -168
@@ -1,4 +1,6 @@
1
1
  import { roleNotFound, usageError } from "../errors/cliError.js";
2
+ import { hasManagedIdentity } from "../runtime/managedIdentity.js";
3
+ import { boundedDocument, recordPage, readOptions, readDigest, messageReceipt } from "../output/boundedRead.js";
2
4
  import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
3
5
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
4
6
  import { createRoleSessionSet, recordRoleAgentSession } from "../executor/agentExecutor.js";
@@ -24,7 +26,15 @@ function inputOutput(output, data) {
24
26
  return { kind: "output", output, data };
25
27
  }
26
28
  function renderGlobalInput(result, options) {
27
- return result.kind === "output" ? jsonOrText(options, result.output, result.data) : result;
29
+ if (result.kind === "output") {
30
+ const record = result.data;
31
+ const failure = record.steer?.code === undefined ? record.interrupt : record.steer;
32
+ if (failure?.code !== undefined)
33
+ options.onInputFailure?.({
34
+ code: failure.code, detail: failure.detail ?? failure.code, data: result.data
35
+ });
36
+ }
37
+ return result;
28
38
  }
29
39
  function assertGlobalInputAuthority(store, options, target) {
30
40
  const env = options.env ?? process.env;
@@ -41,7 +51,7 @@ function assertGlobalInputAuthority(store, options, target) {
41
51
  throw usageError("Global input mutation requires the current native caller Session.");
42
52
  }
43
53
  }
44
- else if (env.YUI_ROLE !== undefined || env.YUI_AGENT_ID !== undefined || env.YUI_NATIVE_SESSION_ID !== undefined) {
54
+ else if (hasManagedIdentity(env)) {
45
55
  throw usageError("Incomplete managed identity cannot acquire user authority.");
46
56
  }
47
57
  }
@@ -71,7 +81,8 @@ export function runGlobalRoleCommand(args, store, options = {}) {
71
81
  function roleContext(args, store, options) {
72
82
  const [rawName, ...rest] = args;
73
83
  const name = roleName(rawName);
74
- assertNoArguments(rest, "Session context usage: yui session context <role>");
84
+ const parsed = readOptions(rest, ["--cursor"]);
85
+ assertNoArguments(parsed.positionals, "Session context usage: yui session context <role> [--cursor <cursor>]");
75
86
  const environment = options.env ?? process.env;
76
87
  // Context reads expose durable input and runtime facts without delivering it.
77
88
  const selfRead = environment.YUI_SESSION_SCOPE !== undefined;
@@ -89,18 +100,12 @@ function roleContext(args, store, options) {
89
100
  const sessions = store.getGlobalRoleSessionSet(name);
90
101
  const session = sessions?.sessions[role.activeAgentId];
91
102
  // Only the controlled Endpoint's accepted receipt consumes a queued Message.
92
- const queued = store.listGlobalRoleMessages(name).filter((message) => message.delivery === undefined
103
+ const messages = store.listGlobalRoleMessages(name);
104
+ const queued = messages.filter((message) => message.delivery === undefined
93
105
  && message.notDelivered === undefined
94
106
  && (message.interruptThen !== undefined || message.inputControl?.action === "queue"));
95
- const pendingQueue = queued.map((message) => ({
96
- id: message.id,
97
- requestId: message.interruptThen?.requestId ?? message.inputControl?.requestId,
98
- body: message.body,
99
- ...(message.control === undefined ? {} : { control: message.control }),
100
- ...(message.deliveryTarget === undefined ? {} : { deliveryTarget: message.deliveryTarget }),
101
- ...(message.interruptThen === undefined ? {} : { interruptThen: true }),
102
- ...(message.delivery === undefined ? {} : { deliveredAt: message.delivery.deliveredAt })
103
- }));
107
+ const pending = recordPage(queued.map(globalMessageSummary), `global:${name}/messages/pending`, { limit: 8 });
108
+ const recent = recordPage([...messages].reverse().map(globalMessageSummary), `global:${name}/messages/all`, { limit: 8 });
104
109
  const context = Object.freeze({
105
110
  schemaVersion: 1,
106
111
  protocol: "yui-managed-context/v1",
@@ -136,28 +141,45 @@ function roleContext(args, store, options) {
136
141
  : ["perform-global-role-request"]
137
142
  },
138
143
  // Reading pending input is neither delivery nor implementation.
139
- pendingMessages: pendingQueue,
140
- messages: store.listGlobalRoleMessages(name),
144
+ pending,
145
+ recent,
146
+ reads: {
147
+ messages: `role message list ${name}`,
148
+ pending: `role message list ${name} --pending`,
149
+ detail: `role message show ${name} <message-id>`
150
+ },
141
151
  nativeTurn: sessions?.providerBinding?.run ?? null,
142
152
  retry: providerRetryProjection(sessions?.providerBinding),
143
- interrupts: sessions?.interrupts ?? {},
153
+ // Historical interrupt receipts are available through Session inspection;
154
+ // only this Turn's exact identity is required at entry.
144
155
  sessionManifestPath: environment.YUI_SESSION_MANIFEST,
145
156
  cliCommand: "yui"
146
157
  });
147
- if (options.jsonOutput === true)
148
- return `${JSON.stringify(context)}\n`;
149
- return [
150
- `Role Context: ${role.name}`,
151
- `Scope: global (${context.identity.roleKind})`,
152
- `Agent: ${binding.agentId}/${binding.adapterId}`,
153
- `Workspace: ${role.workspace}`,
154
- `Effective revision: ${role.launchRevision}`,
155
- `Skills: ${context.profile.skillIds.join(", ") || "none"}`,
156
- `Authority: ${context.authority.view}; Task implementation: no`,
157
- `Pending messages: ${pendingQueue.length === 0 ? "none"
158
- : pendingQueue.map((message) => message.id).join(", ")}`,
159
- ""
160
- ].join("\n");
158
+ const data = boundedDocument(context, `global:${name}/context`, parsed.cursor);
159
+ return { kind: "output", data, output: [
160
+ `Role Context: ${role.name}`,
161
+ `Scope: global (${context.identity.roleKind})`,
162
+ `Agent: ${binding.agentId}/${binding.adapterId}`,
163
+ `Workspace: ${role.workspace}`,
164
+ `Effective revision: ${role.launchRevision}`,
165
+ `Skills: ${context.profile.skillIds.join(", ") || "none"}`,
166
+ `Authority: ${context.authority.view}; Task implementation: no`,
167
+ `Pending messages: ${pending.total}; recent messages: ${recent.total}`,
168
+ JSON.stringify(data),
169
+ ""
170
+ ].join("\n") };
171
+ }
172
+ function globalMessageSummary(message) {
173
+ return {
174
+ id: message.id, createdAt: message.createdAt, kind: message.kind,
175
+ digest: readDigest(message), summary: message.body.slice(0, 240),
176
+ delivery: message.delivery === undefined ? undefined : { deliveredAt: message.delivery.deliveredAt },
177
+ notDelivered: message.notDelivered === undefined ? undefined : { summary: message.notDelivered.reason.slice(0, 240) },
178
+ control: message.control === undefined ? undefined : { outcome: message.control.outcome, observedAt: message.control.observedAt },
179
+ inputAction: message.inputControl?.action,
180
+ deliveryTarget: message.deliveryTarget,
181
+ interruptThen: message.interruptThen === undefined ? undefined : true
182
+ };
161
183
  }
162
184
  function addRole(args, store, options) {
163
185
  const [rawName, ...tail] = args;
@@ -542,6 +564,37 @@ export function sendGlobalRoleMessageCommand(store, roleName, body, author, inpu
542
564
  */
543
565
  function globalRoleMessage(args, store, options) {
544
566
  const [action, rawName, ...tail] = args;
567
+ if (action === "list" || action === "show") {
568
+ const name = roleName(rawName);
569
+ const env = options.env ?? process.env;
570
+ if (env.YUI_SESSION_SCOPE !== undefined
571
+ && (env.YUI_SESSION_SCOPE !== "global" || env.YUI_ROLE !== name)) {
572
+ throw usageError("Global Message read is outside the exact Session scope.");
573
+ }
574
+ requireRole(name, store);
575
+ const pending = tail.includes("--pending");
576
+ const parsed = readOptions(tail.filter(arg => arg !== "--pending"), action === "list"
577
+ ? ["--cursor", "--limit"] : ["--cursor"]);
578
+ const messages = store.listGlobalRoleMessages(name);
579
+ let data;
580
+ if (action === "show") {
581
+ if (parsed.positionals.length !== 1 || pending)
582
+ throw usageError("Role message show <role> <message-id> [--cursor <cursor>].");
583
+ const message = messages.find(m => m.id === parsed.positionals[0]);
584
+ if (message === undefined)
585
+ throw usageError("Global Message is unavailable in this scope.");
586
+ data = boundedDocument(message, `global:${name}/message/${message.id}`, parsed.cursor);
587
+ }
588
+ else {
589
+ if (parsed.positionals.length !== 0)
590
+ throw usageError("Role message list <role> [--pending] [--limit <n>] [--cursor <cursor>].");
591
+ const selected = pending ? messages.filter(message => message.delivery === undefined
592
+ && message.notDelivered === undefined
593
+ && (message.interruptThen !== undefined || message.inputControl?.action === "queue")) : [...messages].reverse();
594
+ data = recordPage(selected.map(globalMessageSummary), `global:${name}/messages/${pending ? "pending" : "all"}`, parsed);
595
+ }
596
+ return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
597
+ }
545
598
  if (action !== "queue" && action !== "steer") {
546
599
  throw usageError("Role message usage: yui role message queue|steer <role> (<body>|--body-file <path|->) --request-id <id> [--expected-target <turn>].");
547
600
  }
@@ -581,14 +634,14 @@ function messageGlobalInput(name, input, store, options) {
581
634
  });
582
635
  if (action === "queue") {
583
636
  const state = persisted.idempotentReplay ? "idempotent-replay" : "queued";
584
- return inputOutput(`Queued Global message ${persisted.message.id} to ${name} (${state}).\n`, { roleName: name, message: persisted.message, delivery: { state } });
637
+ return inputOutput(`Queued Global message ${persisted.message.id} to ${name} (${state}).\n`, { roleName: name, message: messageReceipt(persisted.message), delivery: { state } });
585
638
  }
586
639
  if (persisted.idempotentReplay) {
587
- return inputOutput(`Steer Global message ${persisted.message.id} already recorded (idempotent-replay).\n`, { roleName: name, message: persisted.message, steer: { state: "idempotent-replay" } });
640
+ return inputOutput(`Steer Global message ${persisted.message.id} already recorded (idempotent-replay).\n`, { roleName: name, message: messageReceipt(persisted.message), steer: { state: "idempotent-replay" } });
588
641
  }
589
642
  const resolution = resolveGlobalInputControl(store, name, "steer", expectedTarget);
590
643
  if (resolution.outcome !== "ready") {
591
- return inputOutput(`Steer Global message ${persisted.message.id} saved but not delivered (${resolution.code}: ${resolution.detail}).\n`, { roleName: name, message: persisted.message,
644
+ return inputOutput(`Steer Global message ${persisted.message.id} saved but not delivered (${resolution.code}: ${resolution.detail}).\n`, { roleName: name, message: messageReceipt(persisted.message),
592
645
  steer: { state: "not-steered", code: resolution.code, detail: resolution.detail } });
593
646
  }
594
647
  store.updateGlobalRoleMessage({ ...persisted.message, control: {
@@ -752,17 +805,6 @@ function registerGlobalInterruptThen(store, roleName, thenMessageId, targetAttem
752
805
  }));
753
806
  return "claimed";
754
807
  }
755
- /** Emit a plain string or, under --json, a JSON envelope, matching the Global
756
- * Role command surface's existing string/JSON split. */
757
- function jsonOrText(options, text, data) {
758
- const record = data;
759
- const failure = record.steer?.code === undefined ? record.interrupt : record.steer;
760
- if (failure?.code !== undefined)
761
- options.onInputFailure?.({
762
- code: failure.code, detail: failure.detail ?? failure.code, data
763
- });
764
- return options.jsonOutput === true ? JSON.stringify(data) : text;
765
- }
766
808
  function parseOptions(args, specs) {
767
809
  const values = new Map();
768
810
  const seen = new Set();
@@ -4,6 +4,9 @@ import { createCapabilityGrant, revokeGrant } from "../grant/capabilityGrant.js"
4
4
  import { defaultTableWidth, renderTable } from "../output/table.js";
5
5
  import { formatTimestamp } from "../output/timePresentation.js";
6
6
  import { isCurrentGlobalOperator } from "./taskInputCommands.js";
7
+ import { assertLeaderGrantBounds, taskAuthorizationSource } from "../grant/taskAuthorization.js";
8
+ import { currentManagedRuntime } from "../runtime/managedCaller.js";
9
+ import { isDeepStrictEqual } from "node:util";
7
10
  const CEILINGS = new Set(["none", "reversible", "irreversible"]);
8
11
  export function runGrantCommand(args, store, options) {
9
12
  const [command, ...rest] = args;
@@ -20,7 +23,7 @@ export function runGrantCommand(args, store, options) {
20
23
  }
21
24
  function issueGrant(args, store, options) {
22
25
  const usage = "Task grant issue usage: yui task grant issue <task> --action <name> (repeatable) [--scope-project <id>...] [--scope-repo <owner/name>...] [--scope-package <name>...] [--scope-home <path>] [--param <name=v1,v2>...] [--expires-at <iso-8601>] [--max-uses <int>] [--irreversibility-ceiling <none|reversible|irreversible>].";
23
- const parsed = parseMultiValueTail(args, new Set(["--scope-home", "--expires-at", "--max-uses", "--irreversibility-ceiling"]), new Set(["--action", "--scope-project", "--scope-repo", "--scope-package", "--param"]), usage);
26
+ const parsed = parseMultiValueTail(args, new Set(["--scope-home", "--expires-at", "--max-uses", "--irreversibility-ceiling", "--source-message", "--purpose", "--request-id"]), new Set(["--action", "--scope-project", "--scope-repo", "--scope-package", "--param"]), usage);
24
27
  exactPositionals(parsed.positionals, 1, usage);
25
28
  const taskId = parsed.positionals[0];
26
29
  requireTask(store, taskId);
@@ -32,29 +35,63 @@ function issueGrant(args, store, options) {
32
35
  const expiresAt = optionalOption(parsed.options, "--expires-at");
33
36
  const maxUses = parseMaxUses(parsed, usage);
34
37
  const ceiling = parseCeiling(parsed, usage);
35
- // Grant issuance mints irreversible authority. The granter is bound to the
36
- // authenticated global Operator Session, not to a caller-supplied label.
38
+ // Bind issuance to the authenticated Session, never a caller-supplied label.
37
39
  const env = options.environment ?? {};
38
- const granter = authorizeGrantOrigin(store, env, usage);
40
+ const operator = isCurrentGlobalOperator(store, env);
41
+ const sourceMessage = optionalOption(parsed.options, "--source-message");
42
+ const purpose = optionalOption(parsed.options, "--purpose");
43
+ const requestId = optionalOption(parsed.options, "--request-id");
44
+ if (!operator && !requestId)
45
+ throw usageError("Leader grant issue requires a stable --request-id.");
46
+ const authorizationSource = operator ? undefined
47
+ : taskAuthorizationSource(store, taskId, env, sourceMessage, purpose);
48
+ const granter = operator ? authorizeGrantOrigin(store, env, usage)
49
+ : `leader:${taskId}:${authorizationSource.nativeSessionId}`;
39
50
  const now = clock(options);
40
51
  // The grant record and its audit event commit in one transaction: a failed
41
52
  // event save cannot leave a persisted grant without its audit trail.
42
53
  const grant = store.transaction((tx) => {
43
- const issued = createCapabilityGrant(tx.nextCapabilityGrantId(taskId), taskId, {
54
+ if (operator)
55
+ authorizeGrantOrigin(tx, env, usage);
56
+ else if (!isDeepStrictEqual(authorizationSource, taskAuthorizationSource(tx, taskId, env, sourceMessage, purpose)))
57
+ throw usageError("Authorization source changed before grant issuance; inspect current context.");
58
+ const previous = requestId === undefined ? undefined : tx.listEvents(taskId).find(event => event.type === "capability-grant.issued" && event.payload.requestId === requestId
59
+ && event.payload.issuer === (operator ? "operator" : "leader"));
60
+ const existing = previous === undefined ? null : tx.getCapabilityGrant(taskId, previous.payload.grantId);
61
+ const issued = createCapabilityGrant(existing?.id ?? tx.nextCapabilityGrantId(taskId), taskId, {
44
62
  granter,
45
- ...(scope === undefined ? {} : { scope }),
63
+ ...(authorizationSource === undefined ? {} : { authorizationSource }),
64
+ ...(operator ? (scope === undefined ? {} : { scope }) : { scope: { ...scope, taskId } }),
46
65
  actions,
47
66
  ...(parameterBounds === undefined ? {} : { parameterBounds }),
48
67
  ...(expiresAt === undefined ? {} : { expiresAt }),
49
68
  ...(maxUses === undefined ? {} : { maxUses }),
50
69
  ...(ceiling === undefined ? {} : { irreversibilityCeiling: ceiling })
51
70
  }, now);
71
+ if (previous !== undefined) {
72
+ const identity = (value) => ({
73
+ scope: value.scope, actions: value.actions, parameterBounds: value.parameterBounds,
74
+ expiresAt: value.expiresAt, maxUses: value.maxUses, irreversibilityCeiling: value.irreversibilityCeiling,
75
+ source: value.authorizationSource === undefined ? undefined : {
76
+ messageId: value.authorizationSource.messageId, digest: value.authorizationSource.digest,
77
+ purpose: value.authorizationSource.purpose
78
+ }
79
+ });
80
+ if (!existing || !isDeepStrictEqual(identity(existing), identity(issued))) {
81
+ throw usageError("Grant request id already names different authorization or bounds; inspect the original grant.");
82
+ }
83
+ return existing;
84
+ }
85
+ if (!operator)
86
+ assertLeaderGrantBounds(tx, issued, now);
52
87
  tx.saveCapabilityGrant(taskId, issued);
53
88
  recordTaskEvent(tx, taskId, "capability-grant.issued", {
54
89
  grantId: issued.id,
55
90
  granter: issued.granter,
56
91
  actions: issued.actions.join(","),
57
- scope: scopeSummary(issued.scope)
92
+ scope: scopeSummary(issued.scope),
93
+ ...(requestId === undefined ? {} : { requestId, issuer: operator ? "operator" : "leader" }),
94
+ ...(authorizationSource === undefined ? {} : { authorizationSource: JSON.stringify(authorizationSource) })
58
95
  }, now);
59
96
  return issued;
60
97
  });
@@ -141,7 +178,10 @@ function revokeGrantCommand(args, store, options) {
141
178
  // Revoke is the same irreversible-authority operation as issue: it requires
142
179
  // the authenticated Operator Session, and the revoker is bound to it.
143
180
  const env = options.environment ?? {};
144
- const by = authorizeGrantOrigin(store, env, usage);
181
+ const caller = currentManagedRuntime(store, env, taskId, "leader");
182
+ const leader = caller?.executionAuthority === "delivery"
183
+ && existing.authorizationSource !== undefined && existing.granter.startsWith(`leader:${taskId}:`);
184
+ const by = leader ? `leader:${taskId}:${caller.nativeSessionId}` : authorizeGrantOrigin(store, env, usage);
145
185
  const wasRevoked = existing.revokedAt !== undefined;
146
186
  const now = clock(options);
147
187
  const grant = revokeGrant(existing, by, now);