@zq-silk/yui 1.1.0 → 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 (67) hide show
  1. package/ARCHITECTURE.md +2 -1
  2. package/ARCHITECTURE.zh-CN.md +2 -1
  3. package/README.md +3 -1
  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 +6 -3
  27. package/dist/doctor/ptyProbe.js +74 -0
  28. package/dist/doctor/ptyProbeChild.js +90 -0
  29. package/dist/errors/cliError.js +4 -4
  30. package/dist/errors/cliFailure.js +213 -0
  31. package/dist/executor/fileRoleLaunchPlanner.js +1 -4
  32. package/dist/grant/capabilityGrant.js +13 -0
  33. package/dist/grant/taskAuthorization.js +127 -0
  34. package/dist/kernel/builtinCapabilities.js +60 -8
  35. package/dist/kernel/kernelPorts.js +2 -2
  36. package/dist/output/boundedRead.js +118 -0
  37. package/dist/release/releaseWorkflowEngine.js +8 -1
  38. package/dist/runtime/managedCaller.js +2 -2
  39. package/dist/runtime/runtimeCoherence.js +10 -8
  40. package/dist/storage/sqliteSchema.js +10 -0
  41. package/dist/storage/storageVersions.js +1 -1
  42. package/dist/storage/taskStore.js +3 -0
  43. package/dist/task/leaderArchive.js +111 -0
  44. package/dist/task/leaderArchiveAuthority.js +24 -0
  45. package/dist/tmux/commandExecutor.js +7 -5
  46. package/docs/architecture/README.md +1 -0
  47. package/docs/architecture/README.zh-CN.md +1 -0
  48. package/docs/cli-information-contract.md +107 -0
  49. package/docs/cli-information-contract.zh-CN.md +81 -0
  50. package/docs/managed-turn-and-session-runtime.md +9 -0
  51. package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
  52. package/docs/plugin-sdk.md +7 -3
  53. package/docs/plugin-sdk.zh-CN.md +5 -2
  54. package/docs/release-workflow.md +19 -9
  55. package/docs/release-workflow.zh-CN.md +14 -7
  56. package/docs/storage-baseline.md +5 -0
  57. package/docs/testing/verification-levels.md +12 -2
  58. package/docs/testing/verification-levels.zh-CN.md +8 -0
  59. package/i18n/README.zh-CN.md +3 -1
  60. package/package.json +3 -3
  61. package/skills/yui-leader/SKILL.md +8 -1
  62. package/skills/yui-leader/references/authorization.md +64 -0
  63. package/skills/yui-leader/references/execution.md +18 -4
  64. package/skills/yui-leader/references/task-plugins.md +4 -2
  65. package/skills/yui-operator/SKILL.md +9 -2
  66. package/skills/yui-runtime/SKILL.md +51 -9
  67. package/skills/yui-runtime/references/publication.md +3 -1
@@ -0,0 +1,74 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { fileURLToPath } from "node:url";
5
+ /** Native loading and spawn happen outside Doctor: a native abort cannot kill the CLI. */
6
+ export function inspectPty(options = {}) {
7
+ const started = Date.now();
8
+ const checks = [];
9
+ const major = Number(process.versions.node.split(".")[0]);
10
+ if (![20, 22, 24, 26].includes(major)) {
11
+ checks.push({
12
+ name: "pty Node runtime", status: "unsupported",
13
+ detail: `Node ${process.versions.node} is outside Yui's supported Node 20/22/24/26 range; this is not evidence of a helper permission failure.`
14
+ });
15
+ }
16
+ let entry;
17
+ let identity;
18
+ try {
19
+ const require = createRequire(options.requireFrom ?? import.meta.url);
20
+ entry = require.resolve("node-pty");
21
+ const manifest = require.resolve("node-pty/package.json");
22
+ const version = JSON.parse(readFileSync(manifest, "utf8")).version;
23
+ identity = `node-pty ${version}; entry=${entry}; package=${manifest}`;
24
+ }
25
+ catch (error) {
26
+ return [...checks, {
27
+ name: "node-pty",
28
+ status: error.code === "MODULE_NOT_FOUND" ? "missing" : "invalid",
29
+ detail: `Dependency resolution failed: ${String(error)}`
30
+ }];
31
+ }
32
+ const timeoutMs = options.timeoutMs ?? 1_500;
33
+ const result = spawnSync(process.execPath, [
34
+ fileURLToPath(new URL("./ptyProbeChild.js", import.meta.url)),
35
+ entry, String(options.probeTimeoutMs ?? 750)
36
+ ], {
37
+ // No NODE_OPTIONS, shell startup files, preload hooks or user configuration.
38
+ env: { PATH: "/usr/bin:/bin", LANG: "C" },
39
+ cwd: "/",
40
+ encoding: "utf8",
41
+ timeout: timeoutMs,
42
+ killSignal: "SIGKILL",
43
+ maxBuffer: 64 * 1024
44
+ });
45
+ for (const line of (result.stdout ?? "").split("\n").filter(Boolean)) {
46
+ try {
47
+ const check = JSON.parse(line);
48
+ if (["node-pty", "pty helper", "pty spawn"].includes(check.name)
49
+ && ["ok", "missing", "invalid"].includes(check.status)
50
+ && typeof check.detail === "string") {
51
+ checks.push({ ...check, detail: `${identity}; ${check.detail}` });
52
+ }
53
+ }
54
+ catch { /* Native diagnostics on stdout are retained below on failure. */ }
55
+ }
56
+ const completed = checks.some(check => check.name === "pty spawn")
57
+ || checks.some(check => check.status !== "ok" && check.name !== "pty Node runtime");
58
+ if (result.error || result.signal || result.status !== 0 || !completed) {
59
+ const timedOut = result.error?.code === "ETIMEDOUT";
60
+ // A partial success line is not success if the isolated process failed to finish.
61
+ for (let i = 0; i < checks.length; i += 1) {
62
+ if (checks[i].name === "pty spawn" && checks[i].status === "ok") {
63
+ checks[i] = { ...checks[i], status: "invalid", detail: `${checks[i].detail}; isolated probe did not finish successfully` };
64
+ }
65
+ }
66
+ checks.push({
67
+ name: "pty probe", status: "invalid",
68
+ detail: `${identity}; ${timedOut ? `timeout after ${timeoutMs}ms` : "isolated probe failed"}; exit=${result.status} signal=${result.signal}; ${result.error?.message ?? ""} ${(result.stderr || result.stdout || "").trim()}`
69
+ });
70
+ }
71
+ return checks.map(check => ({
72
+ ...check, detail: `${check.detail}; elapsed=${Date.now() - started}ms`
73
+ }));
74
+ }
@@ -0,0 +1,90 @@
1
+ import { accessSync, constants, statSync, writeSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { basename, dirname, join } from "node:path";
4
+ const require = createRequire(import.meta.url);
5
+ const entry = process.argv[2];
6
+ const timeoutMs = Number(process.argv[3]);
7
+ const report = (check) => writeSync(1, `${JSON.stringify(check)}\n`);
8
+ const errorDetail = (error) => error instanceof Error ? `${error.name}: ${error.message}` : String(error);
9
+ async function probe() {
10
+ let pty;
11
+ try {
12
+ pty = require(entry);
13
+ if (typeof pty.spawn !== "function")
14
+ throw new Error("node-pty does not expose spawn");
15
+ report({ name: "node-pty", status: "ok", detail: "native module loaded" });
16
+ }
17
+ catch (error) {
18
+ report({
19
+ name: "node-pty", status: "invalid",
20
+ detail: `native load failed: ${errorDetail(error)}. If installation reported blocked lifecycle scripts, review npm allow-scripts separately; no install files were changed.`
21
+ });
22
+ return;
23
+ }
24
+ if (process.platform === "darwin") {
25
+ // The pinned node-pty selects build/Release, build/Debug or prebuilds.
26
+ // Inspect the module it actually loaded, not the first helper that exists.
27
+ const native = Object.keys(require.cache).find(path => basename(path) === "pty.node");
28
+ if (!native) {
29
+ report({ name: "pty helper", status: "invalid", detail: "Cannot identify the loaded pty.node; helper path is unverified." });
30
+ return;
31
+ }
32
+ const helper = join(dirname(native), "spawn-helper");
33
+ let mode = "unknown";
34
+ try {
35
+ mode = (statSync(helper).mode & 0o7777).toString(8).padStart(4, "0");
36
+ accessSync(helper, constants.X_OK);
37
+ report({ name: "pty helper", status: "ok", detail: `helper=${helper}; mode=${mode}; executable by current user` });
38
+ }
39
+ catch (error) {
40
+ const code = error.code;
41
+ report({
42
+ name: "pty helper", status: code === "ENOENT" ? "missing" : "invalid",
43
+ detail: `helper=${helper}; mode=${mode}; ${code === "EACCES" ? "permission denied for current user" : "helper check failed"}: ${errorDetail(error)}`
44
+ });
45
+ return;
46
+ }
47
+ }
48
+ await new Promise(resolve => {
49
+ const marker = "yui-pty-probe-ok";
50
+ let terminal;
51
+ let output = "";
52
+ let finished = false;
53
+ const finish = (status, detail, kill = false) => {
54
+ if (finished)
55
+ return;
56
+ finished = true;
57
+ clearTimeout(timer);
58
+ if (kill && terminal) {
59
+ try {
60
+ terminal.kill("SIGKILL");
61
+ }
62
+ catch (error) {
63
+ detail += `; cleanup: ${errorDetail(error)}`;
64
+ }
65
+ }
66
+ report({ name: "pty spawn", status, detail });
67
+ resolve();
68
+ };
69
+ const timer = setTimeout(() => finish("invalid", `timeout after ${timeoutMs}ms waiting for output and normal exit`, true), timeoutMs);
70
+ try {
71
+ // Absolute, fixed executable; no shell, Node preloads, Agent or account access.
72
+ terminal = pty.spawn("/bin/echo", [marker], {
73
+ name: "xterm", cols: 80, rows: 24, cwd: "/",
74
+ env: { PATH: "/usr/bin:/bin", LANG: "C" }
75
+ });
76
+ terminal.onData(data => { output = (output + data).slice(-4096); });
77
+ terminal.onExit(({ exitCode, signal }) => {
78
+ const ok = exitCode === 0 && !signal && output.replace(/\r/g, "").trim() === marker;
79
+ finish(ok ? "ok" : "invalid", `${ok ? "expected output and normal exit confirmed (PTY dependency only; Web Terminal and Provider protocols unverified)" : "unexpected output or exit"}; exit=${exitCode}; signal=${signal ?? 0}; output=${JSON.stringify(output)}`);
80
+ });
81
+ }
82
+ catch (error) {
83
+ finish("invalid", `spawn failed: ${errorDetail(error)}`, true);
84
+ }
85
+ });
86
+ }
87
+ await probe();
88
+ // node-pty's native watcher can retain handles; the fixed child has exited, or
89
+ // was explicitly killed on failure. The parent also bounds native hangs/aborts.
90
+ process.exit(0);
@@ -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
+ }