@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.
- package/ARCHITECTURE.md +2 -1
- package/ARCHITECTURE.zh-CN.md +2 -1
- package/README.md +2 -0
- package/dist/cli/commandCatalog.js +344 -324
- package/dist/cli/commandDiscovery.js +69 -0
- package/dist/cli/completion.js +22 -227
- package/dist/cli/dynamicCompletion.js +12 -6
- package/dist/cli/invocationAuthority.js +2 -2
- package/dist/cli/invocationRouter.js +18 -7
- package/dist/cli/managedDiagnostics.js +2 -1
- package/dist/cli/updatePorts.js +0 -5
- package/dist/cli.js +184 -130
- package/dist/commands/capabilityCommands.js +5 -5
- package/dist/commands/controllerCommands.js +2 -1
- package/dist/commands/globalRoleCommands.js +86 -44
- package/dist/commands/grantCommands.js +48 -8
- package/dist/commands/taskCommands.js +157 -196
- package/dist/commands/taskContextCommand.js +29 -6
- package/dist/commands/taskFactCommands.js +10 -69
- package/dist/commands/taskInputCommands.js +26 -35
- package/dist/commands/taskPublicationCommands.js +2 -35
- package/dist/commands/taskPublicationVerifyCommand.js +1 -2
- package/dist/commands/taskUpstreamCommands.js +2 -1
- package/dist/context/runContextPack.js +8 -29
- package/dist/context/sessionBootstrapManifest.js +5 -101
- package/dist/context/taskContext.js +104 -41
- package/dist/controller/clientRuntime.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +24 -0
- package/dist/controller/runtime.js +6 -0
- package/dist/core/controllerClient.js +46 -21
- package/dist/core/protocol.js +12 -7
- package/dist/doctor/doctor.js +4 -3
- package/dist/errors/cliError.js +4 -4
- package/dist/errors/cliFailure.js +213 -0
- package/dist/executor/fileRoleLaunchPlanner.js +1 -4
- package/dist/grant/capabilityGrant.js +13 -0
- package/dist/grant/taskAuthorization.js +127 -0
- package/dist/kernel/accessAssessment.js +1 -0
- package/dist/kernel/builtinCapabilities.js +88 -9
- package/dist/kernel/capabilityRegistry.js +64 -19
- package/dist/kernel/kernelPorts.js +2 -2
- package/dist/nativeAgent/agent.js +173 -0
- package/dist/nativeAgent/contracts.js +1 -0
- package/dist/nativeAgent/demo.js +29 -0
- package/dist/nativeAgent/index.js +3 -0
- package/dist/nativeAgent/mockProvider.js +43 -0
- package/dist/nativeAgent/textTools.js +156 -0
- package/dist/nativeAgent/validation.js +95 -0
- package/dist/output/boundedRead.js +118 -0
- package/dist/plugins/pluginService.js +98 -8
- package/dist/release/releaseWorkflowEngine.js +8 -1
- package/dist/resources/projectResourceService.js +23 -1
- package/dist/runtime/managedCaller.js +2 -2
- package/dist/runtime/managedIdentity.js +6 -0
- package/dist/runtime/runtimeCoherence.js +10 -8
- package/dist/storage/sqliteSchema.js +10 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +3 -0
- package/dist/surface/surfaceContributions.js +3 -0
- package/dist/task/leaderArchive.js +111 -0
- package/dist/task/leaderArchiveAuthority.js +24 -0
- package/dist/task/taskAuthority.js +5 -10
- package/dist/tmux/commandExecutor.js +7 -5
- package/dist/web/assets/assetManifest.js +38 -22
- package/dist/web/assets/client/api.js +115 -0
- package/dist/web/assets/client/app.js +555 -863
- package/dist/web/assets/client/components.js +323 -897
- package/dist/web/assets/client/detail.js +333 -0
- package/dist/web/assets/client/dock.js +225 -0
- package/dist/web/assets/client/dom.js +55 -3
- package/dist/web/assets/client/evidence.js +172 -0
- package/dist/web/assets/client/format.js +25 -14
- package/dist/web/assets/client/forms.js +230 -0
- package/dist/web/assets/client/i18n.js +1020 -791
- package/dist/web/assets/client/overview.js +115 -0
- package/dist/web/assets/client/records.js +63 -0
- package/dist/web/assets/client/sections.js +313 -0
- package/dist/web/assets/client/sidebar.js +117 -0
- package/dist/web/assets/client/theme.js +61 -22
- package/dist/web/assets/icons.js +39 -0
- package/dist/web/assets/shell.js +124 -118
- package/dist/web/assets/styles/base.js +41 -0
- package/dist/web/assets/styles/components.js +167 -0
- package/dist/web/assets/styles/layout.js +49 -77
- package/dist/web/assets/styles/markdown.js +16 -24
- package/dist/web/assets/styles/responsive.js +33 -44
- package/dist/web/assets/styles/tokens.js +65 -89
- package/dist/web/assets/styles/views.js +289 -0
- package/dist/web/webServer.js +2 -1
- package/docs/architecture/README.md +1 -0
- package/docs/architecture/README.zh-CN.md +1 -0
- package/docs/cli-information-contract.md +107 -0
- package/docs/cli-information-contract.zh-CN.md +81 -0
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
- package/docs/plugin-sdk.md +7 -3
- package/docs/plugin-sdk.zh-CN.md +5 -2
- package/docs/release-workflow.md +42 -9
- package/docs/release-workflow.zh-CN.md +30 -7
- package/docs/storage-baseline.md +5 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +2 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -1
- package/skills/yui-leader/references/authorization.md +64 -0
- package/skills/yui-leader/references/execution.md +18 -4
- package/skills/yui-leader/references/task-plugins.md +4 -2
- package/skills/yui-operator/SKILL.md +9 -2
- package/skills/yui-runtime/SKILL.md +77 -9
- package/skills/yui-runtime/references/publication.md +3 -1
- package/dist/web/assets/client/taskSummary.js +0 -350
- package/dist/web/assets/client/taskSurface.js +0 -616
- package/dist/web/assets/client/view.js +0 -608
- package/dist/web/assets/styles/cards.js +0 -247
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
96
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
""
|
|
160
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
|
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);
|