@zq-silk/yui 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2 -1
- package/ARCHITECTURE.zh-CN.md +2 -1
- package/README.md +2 -0
- package/dist/cli/commandCatalog.js +66 -54
- package/dist/cli/managedDiagnostics.js +2 -1
- package/dist/cli/updatePorts.js +0 -5
- package/dist/cli.js +113 -107
- package/dist/commands/controllerCommands.js +2 -1
- package/dist/commands/globalRoleCommands.js +84 -43
- package/dist/commands/grantCommands.js +48 -8
- package/dist/commands/taskCommands.js +146 -196
- package/dist/commands/taskContextCommand.js +29 -6
- package/dist/commands/taskFactCommands.js +10 -69
- package/dist/commands/taskInputCommands.js +26 -35
- package/dist/commands/taskPublicationCommands.js +2 -35
- package/dist/commands/taskPublicationVerifyCommand.js +1 -2
- package/dist/commands/taskUpstreamCommands.js +2 -1
- package/dist/context/runContextPack.js +8 -29
- package/dist/context/sessionBootstrapManifest.js +5 -101
- package/dist/context/taskContext.js +98 -39
- package/dist/controller/clientRuntime.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +24 -0
- package/dist/controller/runtime.js +6 -0
- package/dist/core/controllerClient.js +46 -21
- package/dist/core/protocol.js +12 -7
- package/dist/doctor/doctor.js +4 -3
- package/dist/errors/cliError.js +4 -4
- package/dist/errors/cliFailure.js +213 -0
- package/dist/executor/fileRoleLaunchPlanner.js +1 -4
- package/dist/grant/capabilityGrant.js +13 -0
- package/dist/grant/taskAuthorization.js +127 -0
- package/dist/kernel/builtinCapabilities.js +60 -8
- package/dist/kernel/kernelPorts.js +2 -2
- package/dist/output/boundedRead.js +118 -0
- package/dist/release/releaseWorkflowEngine.js +8 -1
- package/dist/runtime/managedCaller.js +2 -2
- package/dist/runtime/runtimeCoherence.js +10 -8
- package/dist/storage/sqliteSchema.js +10 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +3 -0
- package/dist/task/leaderArchive.js +111 -0
- package/dist/task/leaderArchiveAuthority.js +24 -0
- package/dist/tmux/commandExecutor.js +7 -5
- package/docs/architecture/README.md +1 -0
- package/docs/architecture/README.zh-CN.md +1 -0
- package/docs/cli-information-contract.md +107 -0
- package/docs/cli-information-contract.zh-CN.md +81 -0
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
- package/docs/plugin-sdk.md +7 -3
- package/docs/plugin-sdk.zh-CN.md +5 -2
- package/docs/release-workflow.md +19 -9
- package/docs/release-workflow.zh-CN.md +14 -7
- package/docs/storage-baseline.md +5 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +2 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -1
- package/skills/yui-leader/references/authorization.md +64 -0
- package/skills/yui-leader/references/execution.md +18 -4
- package/skills/yui-leader/references/task-plugins.md +4 -2
- package/skills/yui-operator/SKILL.md +9 -2
- package/skills/yui-runtime/SKILL.md +51 -9
- package/skills/yui-runtime/references/publication.md +3 -1
|
@@ -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
|
+
}
|
|
@@ -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"
|
|
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: "
|
|
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
|
|
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,6 +443,9 @@ 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;
|
|
@@ -6,14 +6,14 @@ import { InstanceHost } from "./instanceHost.js";
|
|
|
6
6
|
* another Controller, or provide arbitrary persistence to plugin code.
|
|
7
7
|
* T02 registers contributions on this Host and wraps this same Job control.
|
|
8
8
|
*/
|
|
9
|
-
export function createKernelPorts(store, runner, signal = () => undefined, contextProviders = []) {
|
|
9
|
+
export function createKernelPorts(store, runner, signal = () => undefined, contextProviders = [], archiveTask) {
|
|
10
10
|
const host = new InstanceHost();
|
|
11
11
|
const runnerImplementation = host.attach(JOB_RUNNER_IMPLEMENTATION, runner);
|
|
12
12
|
const runnerHandle = host.acquire(runnerImplementation);
|
|
13
13
|
const jobImplementation = host.attach({ id: "yui:job-control", generation: "1" }, createDurableJobControl(store));
|
|
14
14
|
// The Controller is a long-lived consumer of this exact implementation.
|
|
15
15
|
const jobHandle = host.acquire(jobImplementation);
|
|
16
|
-
const capabilities = createBuiltinCapabilities(host, store, jobHandle.value, signal, contextProviders);
|
|
16
|
+
const capabilities = createBuiltinCapabilities(host, store, jobHandle.value, signal, contextProviders, archiveTask);
|
|
17
17
|
return {
|
|
18
18
|
host,
|
|
19
19
|
capabilities,
|