@zq-silk/yui 1.1.2 → 2.0.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 +66 -54
- package/dist/cli/managedDiagnostics.js +2 -1
- package/dist/cli/updatePorts.js +0 -5
- package/dist/cli.js +113 -107
- package/dist/commands/controllerCommands.js +2 -1
- package/dist/commands/globalRoleCommands.js +84 -43
- package/dist/commands/grantCommands.js +48 -8
- package/dist/commands/taskCommands.js +146 -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 +98 -39
- 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/builtinCapabilities.js +60 -8
- package/dist/kernel/kernelPorts.js +2 -2
- package/dist/output/boundedRead.js +118 -0
- package/dist/release/releaseWorkflowEngine.js +8 -1
- package/dist/runtime/managedCaller.js +2 -2
- 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/task/leaderArchive.js +111 -0
- package/dist/task/leaderArchiveAuthority.js +24 -0
- package/dist/tmux/commandExecutor.js +7 -5
- 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 +19 -9
- package/docs/release-workflow.zh-CN.md +14 -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 +51 -9
- package/skills/yui-runtime/references/publication.md +3 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { roleNotFound, usageError } from "../errors/cliError.js";
|
|
2
|
+
import { boundedDocument, recordPage, readOptions, readDigest, messageReceipt } from "../output/boundedRead.js";
|
|
2
3
|
import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
|
|
3
4
|
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
4
5
|
import { createRoleSessionSet, recordRoleAgentSession } from "../executor/agentExecutor.js";
|
|
@@ -24,7 +25,15 @@ function inputOutput(output, data) {
|
|
|
24
25
|
return { kind: "output", output, data };
|
|
25
26
|
}
|
|
26
27
|
function renderGlobalInput(result, options) {
|
|
27
|
-
|
|
28
|
+
if (result.kind === "output") {
|
|
29
|
+
const record = result.data;
|
|
30
|
+
const failure = record.steer?.code === undefined ? record.interrupt : record.steer;
|
|
31
|
+
if (failure?.code !== undefined)
|
|
32
|
+
options.onInputFailure?.({
|
|
33
|
+
code: failure.code, detail: failure.detail ?? failure.code, data: result.data
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
28
37
|
}
|
|
29
38
|
function assertGlobalInputAuthority(store, options, target) {
|
|
30
39
|
const env = options.env ?? process.env;
|
|
@@ -71,7 +80,8 @@ export function runGlobalRoleCommand(args, store, options = {}) {
|
|
|
71
80
|
function roleContext(args, store, options) {
|
|
72
81
|
const [rawName, ...rest] = args;
|
|
73
82
|
const name = roleName(rawName);
|
|
74
|
-
|
|
83
|
+
const parsed = readOptions(rest, ["--cursor"]);
|
|
84
|
+
assertNoArguments(parsed.positionals, "Session context usage: yui session context <role> [--cursor <cursor>]");
|
|
75
85
|
const environment = options.env ?? process.env;
|
|
76
86
|
// Context reads expose durable input and runtime facts without delivering it.
|
|
77
87
|
const selfRead = environment.YUI_SESSION_SCOPE !== undefined;
|
|
@@ -89,18 +99,12 @@ function roleContext(args, store, options) {
|
|
|
89
99
|
const sessions = store.getGlobalRoleSessionSet(name);
|
|
90
100
|
const session = sessions?.sessions[role.activeAgentId];
|
|
91
101
|
// Only the controlled Endpoint's accepted receipt consumes a queued Message.
|
|
92
|
-
const
|
|
102
|
+
const messages = store.listGlobalRoleMessages(name);
|
|
103
|
+
const queued = messages.filter((message) => message.delivery === undefined
|
|
93
104
|
&& message.notDelivered === undefined
|
|
94
105
|
&& (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
|
-
}));
|
|
106
|
+
const pending = recordPage(queued.map(globalMessageSummary), `global:${name}/messages/pending`, { limit: 8 });
|
|
107
|
+
const recent = recordPage([...messages].reverse().map(globalMessageSummary), `global:${name}/messages/all`, { limit: 8 });
|
|
104
108
|
const context = Object.freeze({
|
|
105
109
|
schemaVersion: 1,
|
|
106
110
|
protocol: "yui-managed-context/v1",
|
|
@@ -136,28 +140,45 @@ function roleContext(args, store, options) {
|
|
|
136
140
|
: ["perform-global-role-request"]
|
|
137
141
|
},
|
|
138
142
|
// Reading pending input is neither delivery nor implementation.
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
pending,
|
|
144
|
+
recent,
|
|
145
|
+
reads: {
|
|
146
|
+
messages: `role message list ${name}`,
|
|
147
|
+
pending: `role message list ${name} --pending`,
|
|
148
|
+
detail: `role message show ${name} <message-id>`
|
|
149
|
+
},
|
|
141
150
|
nativeTurn: sessions?.providerBinding?.run ?? null,
|
|
142
151
|
retry: providerRetryProjection(sessions?.providerBinding),
|
|
143
|
-
|
|
152
|
+
// Historical interrupt receipts are available through Session inspection;
|
|
153
|
+
// only this Turn's exact identity is required at entry.
|
|
144
154
|
sessionManifestPath: environment.YUI_SESSION_MANIFEST,
|
|
145
155
|
cliCommand: "yui"
|
|
146
156
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
""
|
|
160
|
-
|
|
157
|
+
const data = boundedDocument(context, `global:${name}/context`, parsed.cursor);
|
|
158
|
+
return { kind: "output", data, output: [
|
|
159
|
+
`Role Context: ${role.name}`,
|
|
160
|
+
`Scope: global (${context.identity.roleKind})`,
|
|
161
|
+
`Agent: ${binding.agentId}/${binding.adapterId}`,
|
|
162
|
+
`Workspace: ${role.workspace}`,
|
|
163
|
+
`Effective revision: ${role.launchRevision}`,
|
|
164
|
+
`Skills: ${context.profile.skillIds.join(", ") || "none"}`,
|
|
165
|
+
`Authority: ${context.authority.view}; Task implementation: no`,
|
|
166
|
+
`Pending messages: ${pending.total}; recent messages: ${recent.total}`,
|
|
167
|
+
JSON.stringify(data),
|
|
168
|
+
""
|
|
169
|
+
].join("\n") };
|
|
170
|
+
}
|
|
171
|
+
function globalMessageSummary(message) {
|
|
172
|
+
return {
|
|
173
|
+
id: message.id, createdAt: message.createdAt, kind: message.kind,
|
|
174
|
+
digest: readDigest(message), summary: message.body.slice(0, 240),
|
|
175
|
+
delivery: message.delivery === undefined ? undefined : { deliveredAt: message.delivery.deliveredAt },
|
|
176
|
+
notDelivered: message.notDelivered === undefined ? undefined : { summary: message.notDelivered.reason.slice(0, 240) },
|
|
177
|
+
control: message.control === undefined ? undefined : { outcome: message.control.outcome, observedAt: message.control.observedAt },
|
|
178
|
+
inputAction: message.inputControl?.action,
|
|
179
|
+
deliveryTarget: message.deliveryTarget,
|
|
180
|
+
interruptThen: message.interruptThen === undefined ? undefined : true
|
|
181
|
+
};
|
|
161
182
|
}
|
|
162
183
|
function addRole(args, store, options) {
|
|
163
184
|
const [rawName, ...tail] = args;
|
|
@@ -542,6 +563,37 @@ export function sendGlobalRoleMessageCommand(store, roleName, body, author, inpu
|
|
|
542
563
|
*/
|
|
543
564
|
function globalRoleMessage(args, store, options) {
|
|
544
565
|
const [action, rawName, ...tail] = args;
|
|
566
|
+
if (action === "list" || action === "show") {
|
|
567
|
+
const name = roleName(rawName);
|
|
568
|
+
const env = options.env ?? process.env;
|
|
569
|
+
if (env.YUI_SESSION_SCOPE !== undefined
|
|
570
|
+
&& (env.YUI_SESSION_SCOPE !== "global" || env.YUI_ROLE !== name)) {
|
|
571
|
+
throw usageError("Global Message read is outside the exact Session scope.");
|
|
572
|
+
}
|
|
573
|
+
requireRole(name, store);
|
|
574
|
+
const pending = tail.includes("--pending");
|
|
575
|
+
const parsed = readOptions(tail.filter(arg => arg !== "--pending"), action === "list"
|
|
576
|
+
? ["--cursor", "--limit"] : ["--cursor"]);
|
|
577
|
+
const messages = store.listGlobalRoleMessages(name);
|
|
578
|
+
let data;
|
|
579
|
+
if (action === "show") {
|
|
580
|
+
if (parsed.positionals.length !== 1 || pending)
|
|
581
|
+
throw usageError("Role message show <role> <message-id> [--cursor <cursor>].");
|
|
582
|
+
const message = messages.find(m => m.id === parsed.positionals[0]);
|
|
583
|
+
if (message === undefined)
|
|
584
|
+
throw usageError("Global Message is unavailable in this scope.");
|
|
585
|
+
data = boundedDocument(message, `global:${name}/message/${message.id}`, parsed.cursor);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
if (parsed.positionals.length !== 0)
|
|
589
|
+
throw usageError("Role message list <role> [--pending] [--limit <n>] [--cursor <cursor>].");
|
|
590
|
+
const selected = pending ? messages.filter(message => message.delivery === undefined
|
|
591
|
+
&& message.notDelivered === undefined
|
|
592
|
+
&& (message.interruptThen !== undefined || message.inputControl?.action === "queue")) : [...messages].reverse();
|
|
593
|
+
data = recordPage(selected.map(globalMessageSummary), `global:${name}/messages/${pending ? "pending" : "all"}`, parsed);
|
|
594
|
+
}
|
|
595
|
+
return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
|
|
596
|
+
}
|
|
545
597
|
if (action !== "queue" && action !== "steer") {
|
|
546
598
|
throw usageError("Role message usage: yui role message queue|steer <role> (<body>|--body-file <path|->) --request-id <id> [--expected-target <turn>].");
|
|
547
599
|
}
|
|
@@ -581,14 +633,14 @@ function messageGlobalInput(name, input, store, options) {
|
|
|
581
633
|
});
|
|
582
634
|
if (action === "queue") {
|
|
583
635
|
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 } });
|
|
636
|
+
return inputOutput(`Queued Global message ${persisted.message.id} to ${name} (${state}).\n`, { roleName: name, message: messageReceipt(persisted.message), delivery: { state } });
|
|
585
637
|
}
|
|
586
638
|
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" } });
|
|
639
|
+
return inputOutput(`Steer Global message ${persisted.message.id} already recorded (idempotent-replay).\n`, { roleName: name, message: messageReceipt(persisted.message), steer: { state: "idempotent-replay" } });
|
|
588
640
|
}
|
|
589
641
|
const resolution = resolveGlobalInputControl(store, name, "steer", expectedTarget);
|
|
590
642
|
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,
|
|
643
|
+
return inputOutput(`Steer Global message ${persisted.message.id} saved but not delivered (${resolution.code}: ${resolution.detail}).\n`, { roleName: name, message: messageReceipt(persisted.message),
|
|
592
644
|
steer: { state: "not-steered", code: resolution.code, detail: resolution.detail } });
|
|
593
645
|
}
|
|
594
646
|
store.updateGlobalRoleMessage({ ...persisted.message, control: {
|
|
@@ -752,17 +804,6 @@ function registerGlobalInterruptThen(store, roleName, thenMessageId, targetAttem
|
|
|
752
804
|
}));
|
|
753
805
|
return "claimed";
|
|
754
806
|
}
|
|
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
807
|
function parseOptions(args, specs) {
|
|
767
808
|
const values = new Map();
|
|
768
809
|
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);
|