@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
@@ -11,8 +11,8 @@ export class CliError extends Error {
11
11
  helpText;
12
12
  details;
13
13
  exitCode;
14
- constructor(code, message, helpText, details = {}) {
15
- super(message);
14
+ constructor(code, message, helpText, details = {}, options) {
15
+ super(message, options);
16
16
  this.code = code;
17
17
  this.helpText = helpText;
18
18
  this.details = details;
@@ -35,6 +35,6 @@ export function agentNotFound(id) {
35
35
  export function dataError(message) {
36
36
  return new CliError("DATA_ERROR", message);
37
37
  }
38
- export function runtimeError(message) {
39
- return new CliError("RUNTIME_ERROR", message);
38
+ export function runtimeError(message, options) {
39
+ return new CliError("RUNTIME_ERROR", message, undefined, {}, options);
40
40
  }
@@ -0,0 +1,213 @@
1
+ import { ControllerClientError } from "../core/controllerClient.js";
2
+ import { ManagedRuntimeDriftError } from "../runtime/managedCaller.js";
3
+ import { redactAgentErrorText } from "../runtime/agentError.js";
4
+ import { CommandExecutionError } from "../tmux/commandExecutor.js";
5
+ import { StorageRecordError, StorageConflictError } from "../storage/taskStore.js";
6
+ import { CliError } from "./cliError.js";
7
+ const RECONCILE = "The operation may have applied; effects are not confirmed by this error. "
8
+ + "Do not replay a write or change its request-id. Read the original Task, Message, operation and receipt "
9
+ + "records using their existing identities before choosing recovery.";
10
+ const INSPECT = "Run `yui doctor` and `yui controller status --verbose` with the same YUI_HOME and matching CLI. ";
11
+ const IDENTITY = "Verify the Home and CLI with `yui version`, `yui controller identity` and `yui doctor`. "
12
+ + "Ask the Operator or user to restore the matching installation/Controller if needed; do not edit identity records.";
13
+ const SESSION = "Use the supplied Session Manifest and exact CLI entry. Read the current scoped context if permitted; "
14
+ + "ask the Operator to inspect/re-enter the current Session if identity cannot be verified. "
15
+ + "Do not forge environment identity, edit the Manifest, or continue writes from a replaced Session.";
16
+ /** Presentation only: no filesystem reads, subprocesses, probes, retries or state changes. */
17
+ export function describeCliFailure(error, context = {}) {
18
+ const clean = (text) => sanitizeFailureText(text, context);
19
+ const chain = [];
20
+ const seen = new Set();
21
+ const pending = [error];
22
+ let controller;
23
+ let session = false;
24
+ let typedCode;
25
+ while (pending.length > 0 && chain.length < 8) {
26
+ const current = pending.shift();
27
+ if (current === undefined || seen.has(current))
28
+ continue;
29
+ seen.add(current);
30
+ if (current instanceof ControllerClientError)
31
+ controller ??= current;
32
+ if (current instanceof ManagedRuntimeDriftError)
33
+ session = true;
34
+ const code = errorCode(current);
35
+ if (code !== undefined && code !== "RUNTIME_ERROR")
36
+ typedCode ??= code;
37
+ chain.push({
38
+ name: current instanceof Error ? clean(current.name) : "Error",
39
+ ...(code === undefined ? {} : { code: clean(code) }),
40
+ // SyntaxError can quote the offending JSON (including tokens and messages).
41
+ message: current instanceof SyntaxError
42
+ ? "Input could not be parsed; raw input omitted."
43
+ : clean(current instanceof Error ? current.message : typeof current === "string" ? current : "Unknown failure (non-error payload omitted).")
44
+ + (current instanceof CommandExecutionError
45
+ ? ` Exit status: ${current.exitStatus ?? "unavailable"}.`
46
+ + (current.stderr.length === 0 ? "" : ` Stderr: ${clean(current.stderr)}`) : "")
47
+ });
48
+ if (current instanceof Error && current.cause !== undefined)
49
+ pending.push(current.cause);
50
+ if (current instanceof AggregateError)
51
+ pending.push(...current.errors.slice(0, 8));
52
+ }
53
+ const code = error instanceof CliError ? error.code : "RUNTIME_ERROR";
54
+ const exitCode = error instanceof CliError ? error.exitCode : 5;
55
+ const diagnostic = {
56
+ ...(context.operation === undefined ? {} : { operation: sanitizeFailureText(context.operation, { environment: context.environment }) }),
57
+ ...(context.home === undefined ? {} : { home: sanitizeFailureText(context.home, { environment: context.environment }) }),
58
+ ...(context.cli === undefined ? {} : { cli: sanitizeFailureText(context.cli, { environment: context.environment }) }),
59
+ ...(context.targets === undefined || context.targets.length === 0 ? {} : { targets: context.targets.map(clean) }),
60
+ causes: chain,
61
+ ...(controller === undefined ? {} : {
62
+ controller: {
63
+ code: controller.code,
64
+ ...sanitizeFailureDetails(controller.diagnostic ?? {}, context)
65
+ }
66
+ })
67
+ };
68
+ const guidanceCode = controller?.code ?? typedCode ?? code;
69
+ const guidance = session ? SESSION
70
+ : error instanceof StorageRecordError ? recoveryGuidance("STORAGE_SCHEMA_INVALID")
71
+ : error instanceof StorageConflictError ? "Read the current record and compare the intended change with its new revision before submitting a new authorized mutation."
72
+ : recoveryGuidance(guidanceCode);
73
+ const delivery = controller?.diagnostic?.delivery;
74
+ const effects = controller === undefined
75
+ ? (code === "USAGE_ERROR" ? "Correct the reported input or prerequisite; this error alone is not a rollback receipt." : RECONCILE)
76
+ : delivery === "not-sent"
77
+ ? "This Controller request was not sent. This does not undo any earlier command steps."
78
+ : delivery === "response-received"
79
+ ? "The Controller returned an error response; it does not prove rollback of earlier effects. " + RECONCILE
80
+ : RECONCILE;
81
+ const help = error instanceof CliError && error.helpText !== undefined ? clean(error.helpText.trimEnd()) : undefined;
82
+ const message = [
83
+ ...chain.map((item, index) => `${index === 0 ? "" : "Caused by: "}${item.code === undefined ? "" : `[${item.code}] `}${item.message}`),
84
+ diagnostic.operation === undefined ? undefined : `Operation: ${diagnostic.operation}`,
85
+ diagnostic.home === undefined ? undefined : `YUI_HOME: ${diagnostic.home}`,
86
+ diagnostic.cli === undefined ? undefined : `CLI entry: ${diagnostic.cli}`,
87
+ diagnostic.targets === undefined ? undefined : `Targets: ${diagnostic.targets.join(", ")}`,
88
+ controller?.diagnostic === undefined ? undefined
89
+ : `Controller request: ${clean(controller.diagnostic.method ?? "unknown method")} id=${clean(controller.diagnostic.requestId ?? "unavailable")} delivery=${delivery}`
90
+ + (controller.diagnostic.socketPath === undefined ? "" : ` socket=${clean(controller.diagnostic.socketPath)}`),
91
+ effects,
92
+ guidance,
93
+ "Recovery commands shown as `yui` must use the same Home and matching CLI entry above; they are not run automatically.",
94
+ help
95
+ ].filter((part) => part !== undefined && part.length > 0).join("\n");
96
+ return {
97
+ code, exitCode, message,
98
+ details: {
99
+ ...(error instanceof CliError ? sanitizeFailureDetails(error.details, context) : {}),
100
+ diagnostic
101
+ }
102
+ };
103
+ }
104
+ export function renderCliFailure(failure, json, jsonCode = failure.code) {
105
+ return json
106
+ ? JSON.stringify({ ok: false, code: jsonCode, message: failure.message, details: failure.details })
107
+ : `${failure.code}: ${failure.message}`;
108
+ }
109
+ function errorCode(error) {
110
+ if (error === null || typeof error !== "object")
111
+ return undefined;
112
+ const code = Reflect.get(error, "code");
113
+ return typeof code === "string" ? code : undefined;
114
+ }
115
+ function recoveryGuidance(code) {
116
+ switch (code) {
117
+ case "CONTROLLER_NOT_RUNNING":
118
+ return INSPECT + "A missing discovery record is not proof of process exit. If confirmed stopped, run `yui start` through the user or Operator.";
119
+ case "CONTROLLER_UNAVAILABLE":
120
+ case "CONTROLLER_DISCOVERY_INVALID":
121
+ return INSPECT + "Process state is unverified. Inspect the reported process/socket ownership before an authorized `yui controller restart`.";
122
+ case "CONTROLLER_ACCESS_DENIED":
123
+ case "EACCES":
124
+ case "EPERM":
125
+ return "Inspect filesystem ownership and the sandbox/OS permission boundary. Ask the user or Operator for authorized access; do not alter permissions automatically.";
126
+ case "CONTROLLER_PROTOCOL_MISMATCH":
127
+ case "CONTROLLER_IDENTITY_MISMATCH":
128
+ case "UNAUTHORIZED":
129
+ return IDENTITY;
130
+ case "CONTROLLER_TIMEOUT":
131
+ case "CONTROLLER_DELIVERY_UNKNOWN":
132
+ case "INVALID_RESPONSE":
133
+ return INSPECT + "Correlate the request with durable records before recovery; a timeout is not proof that the process exited.";
134
+ case "CONTROLLER_HANDOVER_FENCED":
135
+ case "CONTROLLER_HANDOVER_TIMEOUT":
136
+ case "CONTROLLER_DRAINING":
137
+ case "SESSION_BUSY":
138
+ return "Read the current Controller/Session status and existing operation. Let its owner settle the active work; do not start a competing operation.";
139
+ case "STORAGE_UNINITIALIZED":
140
+ return "Verify YUI_HOME first. For an intended new empty Home, the user or Operator can run `yui setup`; do not initialize over existing evidence.";
141
+ case "STORAGE_SCHEMA_INVALID":
142
+ case "STORAGE_SCHEMA_UNSUPPORTED":
143
+ case "STORAGE_FORMAT_UNSUPPORTED":
144
+ return "Run `yui doctor` using this Home and matching CLI. Preserve the database. Use `yui upgrade` only for a diagnosed supported version transition; invalid data needs Operator diagnosis, not reinitialization.";
145
+ case "COMMAND_NOT_FOUND":
146
+ return "Inspect `yui doctor` and the configured tool executable/PATH. Have the user or Operator restore the required dependency; no installation was performed by this diagnostic.";
147
+ case "COMMAND_FAILED":
148
+ case "COMMAND_TIMED_OUT":
149
+ return "Inspect the command cause, exit status and dependency checks with `yui doctor`. A spawn failure alone does not establish missing execute permission; timeout does not prove external effects were undone.";
150
+ case "USAGE_ERROR":
151
+ case "INVALID_PARAMS":
152
+ case "INVALID_REQUEST":
153
+ case "MESSAGE_TOO_LARGE":
154
+ case "METHOD_NOT_FOUND":
155
+ return "Use the command's `--help` and correct its arguments/configuration within the existing authority. Do not bypass scope or validation checks.";
156
+ case "TASK_NOT_FOUND":
157
+ case "ROLE_NOT_FOUND":
158
+ case "AGENT_NOT_FOUND":
159
+ case "NOT_FOUND":
160
+ case "DATA_ERROR":
161
+ return "Verify the named record, prerequisites and current Home using the corresponding show/list/context command. Do not invent or recreate missing state from this error alone.";
162
+ default:
163
+ return "The root cause cannot be confirmed from this error alone. Preserve the operation identity and safe cause details; "
164
+ + "inspect its current records/status and command help, then ask the responsible Agent or Operator to diagnose. No automatic repair or safe retry is implied.";
165
+ }
166
+ }
167
+ export function sanitizeFailureText(text, context = {}) {
168
+ const secrets = [
169
+ ...(context.privateValues ?? []),
170
+ ...Object.entries(context.environment ?? process.env).filter(([name]) => /token|secret|password|passwd|cookie|credential|api_?key|private_?key/i.test(name)).map(([, value]) => value ?? "")
171
+ ].filter(value => value.length > 0).sort((a, b) => b.length - a.length);
172
+ let safe = text;
173
+ for (const value of secrets) {
174
+ // Short invalid operands must not replace every character of a diagnostic.
175
+ const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
176
+ safe = value.length < 4
177
+ ? safe.replace(new RegExp(`(?<![\\w])${escapedValue}(?![\\w])`, "g"), "[REDACTED]")
178
+ : safe.split(value).join("[REDACTED]");
179
+ const escaped = JSON.stringify(value).slice(1, -1);
180
+ if (escaped !== value)
181
+ safe = safe.split(escaped).join("[REDACTED]");
182
+ }
183
+ safe = redactAgentErrorText(safe)
184
+ .replace(/(\b(?:token|credential|body|prompt|content|input)(?:["']?\s*[=:]\s*))("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;}]+)/gi, "$1[REDACTED]")
185
+ .replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@[^\s/]+/gi, "$1[REDACTED]@host")
186
+ .replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/g, "[REDACTED]")
187
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
188
+ .replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "?");
189
+ return safe.length > 12_000 ? safe.slice(0, 12_000) + "…[truncated]" : safe;
190
+ }
191
+ /** Keep existing detail fields, but never dump secret/payload fields or Error internals. */
192
+ export function sanitizeFailureDetails(details, context = {}) {
193
+ const seen = new Set();
194
+ const visit = (value, depth) => {
195
+ if (typeof value === "string")
196
+ return sanitizeFailureText(value, context);
197
+ if (value === null || typeof value === "number" || typeof value === "boolean")
198
+ return value;
199
+ if (typeof value !== "object")
200
+ return undefined;
201
+ if (depth > 32 || seen.has(value))
202
+ return "[omitted]";
203
+ seen.add(value);
204
+ const result = Array.isArray(value) ? value.map(item => visit(item, depth + 1))
205
+ : Object.fromEntries(Object.entries(value).map(([key, item]) => [
206
+ key, /token|secret|password|passwd|cookie|credential|authorization|api.?key|private.?key|environment|^env$|^body$|^payload$|^prompt$|^argv$|^args$/i.test(key)
207
+ ? "[REDACTED]" : visit(item, depth + 1)
208
+ ]));
209
+ seen.delete(value);
210
+ return result;
211
+ };
212
+ return visit(details, 0);
213
+ }
@@ -354,8 +354,7 @@ export class FileRoleLaunchPlanner {
354
354
  developerInstructions: `Read and follow the exact Yui Session Manifest at ${bootstrap.manifestPath}.`,
355
355
  managedContextFile: bootstrap.manifestPath,
356
356
  sessionManifestPath: bootstrap.manifestPath,
357
- sessionManifestDigest: bootstrap.manifest.digest,
358
- sessionCliPath: bootstrap.sessionCliPath
357
+ sessionManifestDigest: bootstrap.manifest.digest
359
358
  };
360
359
  const managedRun = owner.scope === "task" && input.runId !== undefined
361
360
  ? this.store.getRun(owner.taskId, input.runId)
@@ -413,7 +412,6 @@ export class FileRoleLaunchPlanner {
413
412
  const compiled = managedCompiled;
414
413
  for (const path of [
415
414
  bootstrap.manifestPath,
416
- bootstrap.sessionCliPath,
417
415
  bootstrap.roleProfilePath
418
416
  ]) {
419
417
  this.#resourceRegistrar().registerSessionContext(path, {
@@ -557,7 +555,6 @@ export class FileRoleLaunchPlanner {
557
555
  YUI_DRIVER_ID: driver.id,
558
556
  YUI_WORKSPACE: effectiveWorkspace,
559
557
  YUI_SESSION_MANIFEST: sessionContext.sessionManifestPath,
560
- YUI_SESSION_CLI: sessionContext.sessionCliPath,
561
558
  ...(configured.adapterId === "codex"
562
559
  ? { YUI_AGENT_BASE_ARGS: JSON.stringify(configured.baseArgs) }
563
560
  : {}),
@@ -13,6 +13,7 @@ export function createCapabilityGrant(id, taskId, input, now) {
13
13
  id: validateTaskRecordReference({ taskId, localId: id }, "capabilityGrant").localId,
14
14
  taskId: requireIdentity(taskId, "Task id"),
15
15
  granter: requireText(input.granter, "Capability grant granter"),
16
+ ...(input.authorizationSource === undefined ? {} : { authorizationSource: input.authorizationSource }),
16
17
  scope: normalizeScope(input.scope, taskId),
17
18
  actions: normalizeActions(input.actions),
18
19
  parameterBounds: normalizeParameterBounds(input.parameterBounds),
@@ -110,6 +111,18 @@ export function validateCapabilityGrant(grant) {
110
111
  validateTaskRecordReference({ taskId: grant.taskId, localId: grant.id }, "capabilityGrant");
111
112
  requireIdentity(grant.taskId, "Task id");
112
113
  requireText(grant.granter, "Capability grant granter");
114
+ if (grant.authorizationSource !== undefined) {
115
+ const source = grant.authorizationSource;
116
+ if (typeof source !== "object" || source === null
117
+ || Object.keys(source).sort().join(",") !== "digest,messageId,nativeSessionId,purpose") {
118
+ throw new Error("Capability grant authorization source is invalid.");
119
+ }
120
+ validateTaskRecordReference({ taskId: grant.taskId, localId: source.messageId }, "message");
121
+ if (!/^[a-f0-9]{64}$/u.test(source.digest))
122
+ throw new Error("Authorization source digest is invalid.");
123
+ requireText(source.purpose, "Authorization purpose");
124
+ requireText(source.nativeSessionId, "Authorization native Session");
125
+ }
113
126
  validateScope(grant.scope, grant.taskId);
114
127
  normalizeActions(grant.actions);
115
128
  normalizeParameterBounds(grant.parameterBounds);
@@ -0,0 +1,127 @@
1
+ import { createHash } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import { relative, isAbsolute } from "node:path";
4
+ import { usageError } from "../errors/cliError.js";
5
+ import { currentManagedRuntime } from "../runtime/managedCaller.js";
6
+ export function taskAuthorizationSource(store, taskId, environment, messageId, purpose) {
7
+ const caller = currentManagedRuntime(store, environment, taskId, "leader");
8
+ if (caller?.executionAuthority !== "delivery") {
9
+ throw usageError("Authorization requires this Task's current delivery Leader Session.");
10
+ }
11
+ const task = store.getTask(taskId);
12
+ if (!task || task.status === "archived" || task.status === "draft"
13
+ || task.executionGate?.state === "stopped") {
14
+ throw usageError("Task authorization is unavailable in this lifecycle or execution gate.");
15
+ }
16
+ const message = store.listMessages(taskId).find(entry => entry.id === messageId);
17
+ if (!message || !((message.kind === "user" && message.author.type === "user")
18
+ || (message.kind === "operator" && message.author.type === "operator"))) {
19
+ throw usageError("Name an original user or Operator authorization Message in this Task with --source-message; a Role report is not user authority.");
20
+ }
21
+ if (!purpose?.trim() || !message.body.includes(purpose.trim())) {
22
+ throw usageError("--purpose must quote the original explicit authorization verbatim; an Agent's paraphrase is not source evidence.");
23
+ }
24
+ return {
25
+ messageId: message.id,
26
+ // Delivery receipts can settle after admission; they do not change what
27
+ // the user said. Bind the original author/content identity, not transport.
28
+ digest: createHash("sha256").update(JSON.stringify({
29
+ taskId: message.taskId, id: message.id, kind: message.kind,
30
+ author: message.author, body: message.body, createdAt: message.createdAt
31
+ })).digest("hex"),
32
+ purpose: purpose.trim(),
33
+ nativeSessionId: caller.nativeSessionId
34
+ };
35
+ }
36
+ const RELEASE_ACTIONS = new Set([
37
+ "pr-create-or-reuse", "ci-confirm", "merge", "version-tag",
38
+ "npm-publish", "fresh-install-smoke", "post-verify"
39
+ ]);
40
+ const TASK_ACTIONS = new Set([...RELEASE_ACTIONS, "plugin.execute", "resource.local.read", "resource.local.write"]);
41
+ /** Leader grants cannot silently become global grants. Domain consumers still
42
+ * enforce the exact bounds, digest, uses, expiry and irreversible ceiling. */
43
+ export function assertLeaderGrantBounds(store, grant, now) {
44
+ const task = store.getTask(grant.taskId);
45
+ if (task.status !== "active" || task.executionGate?.state !== "enabled") {
46
+ throw usageError("New Leader grants require an active Task with execution enabled.");
47
+ }
48
+ if (!grant.expiresAt || Date.parse(grant.expiresAt) <= now.getTime() || !grant.maxUses) {
49
+ throw usageError("Leader grants require a future --expires-at and finite --max-uses.");
50
+ }
51
+ if (grant.scope.taskId !== grant.taskId
52
+ || grant.scope.projectIds?.some(id => !task.projectBindings.some(binding => binding.projectId === id))
53
+ || grant.actions.some(action => !TASK_ACTIONS.has(action))) {
54
+ throw usageError("Grant exceeds this Task's Project or action authority; global operations require Operator.");
55
+ }
56
+ const bounds = (names) => {
57
+ for (const name of names) {
58
+ if (!grant.parameterBounds[name]?.length)
59
+ throw usageError(`Leader grant requires exact --param ${name}=... bounds.`);
60
+ }
61
+ };
62
+ if (grant.actions.some(action => RELEASE_ACTIONS.has(action))) {
63
+ bounds(["sourceCommit"]);
64
+ if (grant.actions.includes("post-verify"))
65
+ bounds(["command"]);
66
+ if (grant.parameterBounds.sourceCommit.some(commit => !/^[a-f0-9]{40}$/u.test(commit))) {
67
+ throw usageError("Release sourceCommit bounds must be exact 40-hex commits.");
68
+ }
69
+ if (!grant.scope.projectIds?.length || !grant.scope.repositories?.length || grant.scope.homePath) {
70
+ throw usageError("Release grants require explicit Project/repository scope and cannot target a Home.");
71
+ }
72
+ const repositories = grant.scope.projectIds.map(id => store.getProject(id)?.remoteUrl)
73
+ .map(url => url?.replace(/\.git$/u, "").match(/[:/]([^/:]+\/[^/]+)$/u)?.[1]);
74
+ if (grant.scope.repositories.some(repo => !repositories.includes(`${repo.owner}/${repo.name}`))) {
75
+ throw usageError("Repository is outside the selected Task Projects.");
76
+ }
77
+ if (grant.actions.some(action => ["npm-publish", "fresh-install-smoke", "version-tag"].includes(action)))
78
+ bounds(["version"]);
79
+ if (grant.parameterBounds.version?.some(version => !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(version))) {
80
+ throw usageError("Release version bounds must name concrete versions, not tags or ranges.");
81
+ }
82
+ if (grant.actions.some(action => ["npm-publish", "fresh-install-smoke"].includes(action))
83
+ && !grant.scope.packages?.length)
84
+ throw usageError("Package effects require explicit --scope-package.");
85
+ }
86
+ if (grant.actions.includes("plugin.execute")) {
87
+ bounds(["pluginId", "digest", "environmentRef", "trust", "phase"]);
88
+ if (grant.parameterBounds.environmentRef.some(ref => !ref.startsWith(`${grant.taskId}/`))
89
+ || grant.parameterBounds.trust.some(trust => trust !== "trusted-local")
90
+ || grant.parameterBounds.phase.some(phase => !["build", "validate", "activate", "call"].includes(phase))) {
91
+ throw usageError("Plugin grant has an out-of-Task environment, trust or phase.");
92
+ }
93
+ }
94
+ if (grant.actions.some(action => action.startsWith("resource.local."))) {
95
+ bounds(["resourceId"]);
96
+ if (!grant.scope.homePath)
97
+ throw usageError("Local resource grants require an exact canonical --scope-home path.");
98
+ const path = assertTaskLocalResourcePath(store, grant.scope.homePath);
99
+ if (grant.parameterBounds.resourceId.some(id => store.getLocalResource(id)?.path !== path)) {
100
+ throw usageError("Resource identity does not match the authorized canonical path.");
101
+ }
102
+ }
103
+ else if (grant.scope.homePath) {
104
+ throw usageError("A Leader cannot grant Home access.");
105
+ }
106
+ }
107
+ export function assertTaskLocalResourcePath(store, path) {
108
+ const canonical = realpathSync(path);
109
+ if (canonical !== path)
110
+ throw usageError("Use the exact canonical resource directory path.");
111
+ const overlaps = (other) => {
112
+ const contains = (parent, child) => {
113
+ const rel = relative(parent, child);
114
+ return rel === "" || (rel !== ".." && !rel.startsWith("../") && !isAbsolute(rel));
115
+ };
116
+ return contains(canonical, other) || contains(other, canonical);
117
+ };
118
+ const protectedPaths = [
119
+ realpathSync(store.rootDirectory()), ...store.listProjects().map(project => project.path),
120
+ ...store.listTasks().flatMap(task => store.listManagedWorkspaces(task.id)
121
+ .flatMap(workspace => [workspace.root, ...workspace.entries.map(entry => entry.path)]))
122
+ ];
123
+ if (protectedPaths.some(overlaps)) {
124
+ throw usageError("Resource overlaps a Yui Home, stable Project or managed workspace; use its existing authority.");
125
+ }
126
+ return canonical;
127
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,10 @@
1
+ import { assertTaskLocalResourcePath, taskAuthorizationSource } from "../grant/taskAuthorization.js";
2
+ import { createTaskEvent } from "../event/taskEvent.js";
1
3
  import { listArtifactsCapability, readArtifactCapability, saveArtifactCapability } from "../artifacts/artifactCapability.js";
2
4
  import { runConfigCommand } from "../commands/configCommands.js";
3
5
  import { sendTaskMessageCommand, updateTaskMetadataCommand } from "../commands/taskCommands.js";
4
6
  import { CONFIG_DOMAINS } from "../config/configCatalog.js";
5
- import { inspectTaskContext, readTaskContext, readTaskContextDelta, withContextObservations } from "../context/taskContext.js";
7
+ import { inspectTaskContext, listTaskContext, readTaskContext, readTaskContextDelta, withContextObservations } from "../context/taskContext.js";
6
8
  import { createJobCallAuthority, parseDurableJobStartParams } from "../controller/jobControl.js";
7
9
  import { inspectJobOperation } from "../job/jobOperation.js";
8
10
  import { assertJobAssignmentScope } from "../job/jobAssignmentScope.js";
@@ -41,11 +43,18 @@ const definitions = [
41
43
  inputSchema: object({ taskId: text, after: text, continuation: text, limit: { type: "integer" } }, ["taskId", "after"]),
42
44
  outputSchema: { type: "object", required: ["events", "throughCursor"] }
43
45
  },
46
+ {
47
+ name: "context.list", summary: "Discover one authorized record family with bounded summaries and exact references.",
48
+ effect: "query", requiredPermissions: ["task:read"], source: "listTaskContext",
49
+ inputSchema: object({ taskId: text, store: text, cursor: text, limit: { type: "integer" },
50
+ status: text, after: text, workItemId: text }, ["taskId", "store"]),
51
+ outputSchema: { type: "object", required: ["items", "total", "complete", "nextCursor"] }
52
+ },
44
53
  {
45
54
  name: "context.inspect", summary: "Expand an authorized current record; optionally require its exact digest.",
46
55
  effect: "query", requiredPermissions: ["task:read"], source: "inspectTaskContext",
47
- inputSchema: object({ taskId: text, store: text, refId: text, digest: text }, ["taskId", "store", "refId"]),
48
- outputSchema: { type: "object", required: ["ref", "value", "coreCursor"] }
56
+ inputSchema: object({ taskId: text, store: text, refId: text, digest: text, cursor: text }, ["taskId", "store", "refId"]),
57
+ outputSchema: { type: "object" }
49
58
  },
50
59
  {
51
60
  name: "artifact.save", summary: "Save a file artifact by relativePath into the Task's local Git repository; returns a commit-pinned reference.",
@@ -94,9 +103,14 @@ const definitions = [
94
103
  inputSchema: taskInput, outputSchema: { type: "array", items: recordOutput }
95
104
  },
96
105
  {
97
- name: "resource.local.register", summary: "Register a user-owned local directory by canonical identity (Operator); registration does not grant use.",
106
+ name: "task.archive", summary: "Archive this terminal Task with original user authorization; may end the requesting Leader Session. Inspect durable archive receipts after interrupted delivery.",
107
+ effect: "local-mutation", requiredPermissions: ["task:manage"], source: "LeaderArchive",
108
+ inputSchema: object({ taskId: text, sourceMessage: text, purpose: text }), outputSchema: recordOutput
109
+ },
110
+ {
111
+ name: "resource.local.register", summary: "Register a user-owned local directory by canonical identity (Operator or source-authorized Task Leader); registration does not grant use.",
98
112
  effect: "local-mutation", requiredPermissions: ["resource:register"], source: "ProjectResources.registerLocalDirectory",
99
- inputSchema: object({ displayName: text, path: text }), outputSchema: recordOutput
113
+ inputSchema: object({ displayName: text, path: text, sourceMessage: text, purpose: text }, ["displayName", "path"]), outputSchema: recordOutput
100
114
  },
101
115
  {
102
116
  name: "resource.local.read", summary: "Read an explicitly granted local resource.",
@@ -217,7 +231,7 @@ export const BUILTIN_CAPABILITIES = definitions.map((entry) => ({
217
231
  }));
218
232
  /** One registry with separate managed credentials and in-process Web query
219
233
  * issuance. Socket possession or caller-scope JSON never issues a Web context. */
220
- export function createBuiltinCapabilities(host, store, jobs, signal = () => undefined, contextProviders = []) {
234
+ export function createBuiltinCapabilities(host, store, jobs, signal = () => undefined, contextProviders = [], archiveTask) {
221
235
  const authority = createJobCallAuthority(store);
222
236
  const resources = createProjectResources(store);
223
237
  const callers = new WeakMap();
@@ -243,6 +257,16 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
243
257
  throw new Error("Capability target is outside the authenticated Task.");
244
258
  }
245
259
  const taskId = invocation.context.targetId;
260
+ if (name === "task.archive") {
261
+ if (!archiveTask)
262
+ throw new Error("Controller archive execution is unavailable.");
263
+ if (!invocation.requestId)
264
+ throw new Error("Archive requires a stable requestId.");
265
+ return archiveTask(taskId, callerEnvironment(caller), {
266
+ sourceMessage: params.sourceMessage, purpose: params.purpose,
267
+ requestId: invocation.requestId
268
+ });
269
+ }
246
270
  if (name === "message.send") {
247
271
  // A managed Task Session resolves to Leader/role authority and never
248
272
  // gains develop from an intent argument; only a global Operator Session
@@ -277,9 +301,16 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
277
301
  after: params.after, continuation: params.continuation,
278
302
  limit: params.limit
279
303
  }, callerEnvironment(caller));
304
+ if (name === "context.list")
305
+ return listTaskContext(store, taskId, params.store, {
306
+ cursor: params.cursor, limit: params.limit,
307
+ status: params.status, after: params.after,
308
+ workItemId: params.workItemId
309
+ }, callerEnvironment(caller));
280
310
  if (name === "context.inspect")
281
311
  return inspectTaskContext(store, taskId, {
282
- store: params.store, refId: params.refId, digest: params.digest
312
+ store: params.store, refId: params.refId, digest: params.digest,
313
+ cursor: params.cursor
283
314
  }, callerEnvironment(caller));
284
315
  // File/directory artifacts live in the Task's local Git repository, not
285
316
  // the DB. Save commits exactly one path and returns a self-certifying
@@ -316,7 +347,18 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
316
347
  if (name === "environment.list")
317
348
  return store.listEnvironmentPreparations(taskId);
318
349
  if (name === "resource.local.register")
319
- return resources.registerLocalDirectory(params.displayName, params.path);
350
+ return store.transaction(tx => {
351
+ const source = caller.scope === "task"
352
+ ? taskAuthorizationSource(tx, taskId, callerEnvironment(caller), params.sourceMessage, params.purpose)
353
+ : undefined;
354
+ if (source)
355
+ assertTaskLocalResourcePath(tx, params.path);
356
+ const resource = resources.registerLocalDirectory(params.displayName, params.path);
357
+ if (source)
358
+ tx.saveEvent(taskId, createTaskEvent(tx.nextEventId(taskId), taskId, "resource.local.registered", { resourceId: resource.id, path: resource.path,
359
+ authorizationSource: JSON.stringify(source) }, new Date()));
360
+ return resource;
361
+ });
320
362
  if (name === "resource.local.read")
321
363
  return resources.readLocalResource(taskId, params.resourceId);
322
364
  if (name === "project.context")
@@ -375,6 +417,13 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
375
417
  throw new Error("Capability target is outside the authenticated Task.");
376
418
  }
377
419
  for (const permission of descriptor?.requiredPermissions ?? []) {
420
+ if (caller.scope === "task" && descriptor?.effect !== "query"
421
+ && ["task:manage", "plugin:manage", "resource:register"].includes(permission)) {
422
+ const sessions = store.getTaskRoleSessionSet(task.id, caller.role);
423
+ if (sessions?.sessions[sessions.activeAgentId]?.effective.executionAuthority !== "delivery") {
424
+ throw new Error("Planning Sessions cannot mutate delivery environments or plugins.");
425
+ }
426
+ }
378
427
  if (permission === "job:start" && caller.scope === "task") {
379
428
  const sessions = store.getTaskRoleSessionSet(task.id, caller.role);
380
429
  if (sessions?.sessions[sessions.activeAgentId]?.effective.executionAuthority !== "delivery") {
@@ -394,13 +443,43 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
394
443
  // adopted environments and exact plugin.execute grants remain separate.
395
444
  if (permission === "plugin:manage" && caller.scope === "task" && caller.role === "leader")
396
445
  continue;
446
+ if (permission === "resource:register" && descriptor?.name === "resource.local.register"
447
+ && caller.scope === "task" && caller.role === "leader")
448
+ continue;
397
449
  if ((permission === "config:read" || permission === "plugin:manage" || permission === "resource:register")
398
450
  && caller.scope === "global" && caller.role === "operator")
399
451
  continue;
400
452
  throw new Error(`Permission unavailable: ${permission}.`);
401
453
  }
402
454
  return { taskIds: [task.id], projectIds: task.projectBindings.map((binding) => binding.projectId) };
403
- }, BUILTIN_CAPABILITIES);
455
+ }, BUILTIN_CAPABILITIES, (context, descriptor, input) => {
456
+ const caller = current(context);
457
+ const params = input;
458
+ const via = caller.scope === "global" ? "operator"
459
+ : caller.role === "leader" ? "leader-input" : "leader-message";
460
+ const forCaller = (access) => caller.scope === "user" && access.state === "requestable" ? { state: "hidden" } : access;
461
+ const pluginAccess = plugins.assess(context.targetId, descriptor, input, via);
462
+ if (pluginAccess)
463
+ return forCaller(pluginAccess);
464
+ if (input === undefined)
465
+ return undefined;
466
+ if (descriptor.name === "resource.local.read") {
467
+ return forCaller(resources.assessLocalAccess(context.targetId, params.resourceId, "read", via));
468
+ }
469
+ if (descriptor.name === "environment.prepare") {
470
+ const plan = params.plan;
471
+ return plan.kind === "local"
472
+ ? forCaller(resources.assessLocalAccess(context.targetId, plan.resourceId, plan.access, via))
473
+ : { state: "authorized" };
474
+ }
475
+ if (descriptor.name === "task.read" || descriptor.name === "config.read"
476
+ || descriptor.name === "resource.workspaces" || descriptor.name === "environment.list") {
477
+ return { state: "authorized" };
478
+ }
479
+ // Other domain operations retain their exact execution checks. Do not
480
+ // claim target authorization from a descriptor's coarse permission alone.
481
+ return undefined;
482
+ });
404
483
  const plugins = createPluginService(store, host, registry);
405
484
  return {
406
485
  registry,