@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.
Files changed (64) 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 +66 -54
  5. package/dist/cli/managedDiagnostics.js +2 -1
  6. package/dist/cli/updatePorts.js +0 -5
  7. package/dist/cli.js +113 -107
  8. package/dist/commands/controllerCommands.js +2 -1
  9. package/dist/commands/globalRoleCommands.js +84 -43
  10. package/dist/commands/grantCommands.js +48 -8
  11. package/dist/commands/taskCommands.js +146 -196
  12. package/dist/commands/taskContextCommand.js +29 -6
  13. package/dist/commands/taskFactCommands.js +10 -69
  14. package/dist/commands/taskInputCommands.js +26 -35
  15. package/dist/commands/taskPublicationCommands.js +2 -35
  16. package/dist/commands/taskPublicationVerifyCommand.js +1 -2
  17. package/dist/commands/taskUpstreamCommands.js +2 -1
  18. package/dist/context/runContextPack.js +8 -29
  19. package/dist/context/sessionBootstrapManifest.js +5 -101
  20. package/dist/context/taskContext.js +98 -39
  21. package/dist/controller/clientRuntime.js +16 -8
  22. package/dist/controller/fileSchedulerStoreAdapter.js +24 -0
  23. package/dist/controller/runtime.js +6 -0
  24. package/dist/core/controllerClient.js +46 -21
  25. package/dist/core/protocol.js +12 -7
  26. package/dist/doctor/doctor.js +4 -3
  27. package/dist/errors/cliError.js +4 -4
  28. package/dist/errors/cliFailure.js +213 -0
  29. package/dist/executor/fileRoleLaunchPlanner.js +1 -4
  30. package/dist/grant/capabilityGrant.js +13 -0
  31. package/dist/grant/taskAuthorization.js +127 -0
  32. package/dist/kernel/builtinCapabilities.js +60 -8
  33. package/dist/kernel/kernelPorts.js +2 -2
  34. package/dist/output/boundedRead.js +118 -0
  35. package/dist/release/releaseWorkflowEngine.js +8 -1
  36. package/dist/runtime/managedCaller.js +2 -2
  37. package/dist/runtime/runtimeCoherence.js +10 -8
  38. package/dist/storage/sqliteSchema.js +10 -0
  39. package/dist/storage/storageVersions.js +1 -1
  40. package/dist/storage/taskStore.js +3 -0
  41. package/dist/task/leaderArchive.js +111 -0
  42. package/dist/task/leaderArchiveAuthority.js +24 -0
  43. package/dist/tmux/commandExecutor.js +7 -5
  44. package/docs/architecture/README.md +1 -0
  45. package/docs/architecture/README.zh-CN.md +1 -0
  46. package/docs/cli-information-contract.md +107 -0
  47. package/docs/cli-information-contract.zh-CN.md +81 -0
  48. package/docs/managed-turn-and-session-runtime.md +9 -0
  49. package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
  50. package/docs/plugin-sdk.md +7 -3
  51. package/docs/plugin-sdk.zh-CN.md +5 -2
  52. package/docs/release-workflow.md +19 -9
  53. package/docs/release-workflow.zh-CN.md +14 -7
  54. package/docs/storage-baseline.md +5 -0
  55. package/docs/testing/verification-levels.md +2 -2
  56. package/i18n/README.zh-CN.md +2 -0
  57. package/package.json +1 -1
  58. package/skills/yui-leader/SKILL.md +8 -1
  59. package/skills/yui-leader/references/authorization.md +64 -0
  60. package/skills/yui-leader/references/execution.md +18 -4
  61. package/skills/yui-leader/references/task-plugins.md +4 -2
  62. package/skills/yui-operator/SKILL.md +9 -2
  63. package/skills/yui-runtime/SKILL.md +51 -9
  64. 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
- return result.kind === "output" ? jsonOrText(options, result.output, result.data) : result;
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
- assertNoArguments(rest, "Session context usage: yui session context <role>");
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 queued = store.listGlobalRoleMessages(name).filter((message) => message.delivery === undefined
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 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
- }));
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
- pendingMessages: pendingQueue,
140
- messages: store.listGlobalRoleMessages(name),
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
- interrupts: sessions?.interrupts ?? {},
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
- 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");
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
- // 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);