@zq-silk/yui 2.10.0 → 2.12.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 (90) hide show
  1. package/dist/artifacts/artifactCapability.js +14 -8
  2. package/dist/artifacts/artifactCommitLock.js +7 -5
  3. package/dist/artifacts/jobArtifact.js +93 -0
  4. package/dist/artifacts/jobArtifactTransport.js +13 -0
  5. package/dist/artifacts/taskArtifactRepository.js +58 -10
  6. package/dist/cli/callerDiscovery.js +18 -0
  7. package/dist/cli/commandCatalog.js +45 -14
  8. package/dist/cli/commandDiscovery.js +2 -3
  9. package/dist/commands/durableJobCommands.js +31 -6
  10. package/dist/commands/globalRoleCommands.js +5 -0
  11. package/dist/commands/scheduleCommands.js +14 -3
  12. package/dist/commands/taskCommands.js +5 -2
  13. package/dist/context/taskCatalog.js +6 -1
  14. package/dist/controlPlaneCli.js +12 -12
  15. package/dist/controller/agentJobControl.js +391 -0
  16. package/dist/controller/controller.js +24 -3
  17. package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
  18. package/dist/controller/globalInputDelivery.js +8 -1
  19. package/dist/controller/jobArtifactControl.js +111 -0
  20. package/dist/controller/jobControl.js +4 -0
  21. package/dist/controller/jobDefinitionControl.js +191 -19
  22. package/dist/controller/jobSupervisor.js +83 -3
  23. package/dist/controller/jobTaskDelegationControl.js +193 -0
  24. package/dist/controller/runtime.js +7 -1
  25. package/dist/controller/scheduleControl.js +83 -20
  26. package/dist/integration/gitIntegrationService.js +2 -0
  27. package/dist/job/agentJob.js +131 -0
  28. package/dist/job/agentJobObservation.js +61 -0
  29. package/dist/job/agentRoleScope.js +33 -0
  30. package/dist/job/durableJob.js +63 -28
  31. package/dist/job/jobAssignmentScope.js +1 -0
  32. package/dist/job/jobBusinessResult.js +72 -0
  33. package/dist/job/jobCleanupInspection.js +4 -0
  34. package/dist/job/jobDefinition.js +26 -2
  35. package/dist/job/jobRunner.js +4 -3
  36. package/dist/job/projectRefreshRunner.js +54 -18
  37. package/dist/job/projectSync.js +162 -0
  38. package/dist/job/scheduleService.js +55 -0
  39. package/dist/job/taskDelegation.js +48 -0
  40. package/dist/message/messageContinuation.js +1 -1
  41. package/dist/resources/projectResourceService.js +1 -1
  42. package/dist/resources/resourceGc.js +1 -1
  43. package/dist/role/role.js +22 -12
  44. package/dist/runtime/managedIdentity.js +5 -3
  45. package/dist/runtime/sessionCaller.js +9 -10
  46. package/dist/schedule/schedule.js +65 -15
  47. package/dist/scheduler/wakeReason.js +4 -0
  48. package/dist/storage/agentJobSchema.js +10 -0
  49. package/dist/storage/maintenanceCaller.js +17 -25
  50. package/dist/storage/nativeSessionLookup.js +23 -0
  51. package/dist/storage/recordValidation.js +2 -0
  52. package/dist/storage/sessionHistorySchema.js +17 -0
  53. package/dist/storage/sqliteSchema.js +96 -11
  54. package/dist/storage/sqliteStore.js +71 -4
  55. package/dist/storage/storageVersions.js +1 -1
  56. package/dist/storage/storeRpc.js +4 -0
  57. package/dist/storage/taskCatalog.js +10 -2
  58. package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
  59. package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
  60. package/dist/task/archivePreflight.js +15 -0
  61. package/dist/task/completionReadiness.js +7 -0
  62. package/dist/task/task.js +17 -1
  63. package/dist/verification/verificationGateService.js +4 -0
  64. package/dist/web/assets/client/app/catalog.js +16 -0
  65. package/dist/web/assets/client/i18n/en.js +29 -2
  66. package/dist/web/assets/client/i18n/zh-CN.js +29 -2
  67. package/dist/web/assets/client/layout/manageShell.js +9 -0
  68. package/dist/web/assets/client/layout/router.js +1 -0
  69. package/dist/web/assets/client/views/jobs.js +166 -37
  70. package/dist/web/assets/client/views/task/files.js +5 -4
  71. package/dist/web/assets/client/views/task/page.js +5 -1
  72. package/dist/web/taskPreviews.js +2 -2
  73. package/dist/web/webJobs.js +36 -11
  74. package/dist/web/webMaterials.js +21 -9
  75. package/dist/web/webServer.js +6 -5
  76. package/dist/web/webTaskSurface.js +1 -0
  77. package/docs/authorization.md +130 -0
  78. package/docs/job-runs.md +392 -17
  79. package/docs/managed-turn-and-session-runtime.md +10 -5
  80. package/docs/managed-turn-and-session-runtime.zh-CN.md +12 -5
  81. package/docs/release-workflow.md +79 -0
  82. package/docs/release-workflow.zh-CN.md +58 -0
  83. package/docs/schedules.md +69 -15
  84. package/docs/storage-baseline.md +45 -1
  85. package/docs/storage-baseline.zh-CN.md +33 -1
  86. package/docs/testing/verification-levels.md +2 -2
  87. package/docs/testing/verification-levels.zh-CN.md +2 -2
  88. package/package.json +1 -1
  89. package/skills/yui-runtime/SKILL.md +12 -5
  90. package/skills/yui-runtime/VERSION +1 -1
@@ -5,8 +5,8 @@ import { requireKnownFields } from "../domain/validation.js";
5
5
  import { WebRequestRejected, webLocalMutation } from "./webMutation.js";
6
6
  /** Local-user ingress only, behind the Web token/origin boundary. Never use
7
7
  * this adapter for managed RPC callers. The route fixes scope; the server
8
- * resolves the current Operator binding on every request. No invented Session
9
- * or durable second authority: the public services revalidate the binding.
8
+ * uses public user authority for global controlled actions. Task shell access
9
+ * still resolves the current Operator binding; no invented Session.
10
10
  */
11
11
  export function createWebJobs(store, home) {
12
12
  function invoke(scope, family, value, write, now) {
@@ -14,21 +14,31 @@ export function createWebJobs(store, home) {
14
14
  if (!value || typeof value !== "object" || Array.isArray(value))
15
15
  throw new Error("Expected an operation.");
16
16
  requireKnownFields(value, family === "jobs"
17
- ? ["op", "id", "title", "configuration", "expectedRevision", "runId", "requestId"]
18
- : ["op", "id", "jobId", "rule", "expectedRevision"], "Web Job/Schedule request");
17
+ ? ["op", "id", "title", "configuration", "expectedRevision", "runId", "requestId",
18
+ "reference", "offset", "expectedSessionId"]
19
+ : ["op", "id", "jobId", "rule", "expectedRevision", "authorization", "target"], "Web Job/Schedule request");
19
20
  const input = value;
20
- const reads = family === "jobs" ? ["list", "show", "runs", "run-get", "logs"] : ["list", "show"];
21
- const writes = family === "jobs" ? ["define", "update", "run", "run-cancel"] : ["create", "update", "enable", "disable"];
21
+ const reads = family === "jobs" ? ["list", "show", "runs", "run-get", "run-result", "run-evidence",
22
+ "run-delegation", "logs", "artifact-list", "artifact-preview", "session-inspect"] : ["list", "show"];
23
+ const writes = family === "jobs" ? ["define", "update", "run", "run-cancel", "artifact-register",
24
+ "session-stop", "session-new"] : ["create", "update", "enable", "disable"];
22
25
  if (!(write ? writes : reads).includes(String(input.op)))
23
26
  throw new Error("Operation is not available on this endpoint.");
24
27
  if (scope.kind === "task" && !tx.getTask(scope.taskId))
25
28
  throw new Error("Task not found.");
26
- const session = activeLiveRoleAgentSession(tx.getGlobalRoleSessionSet("operator"));
27
- if (!session)
28
- throw new Error("A current Operator Session is required for Job management.");
29
- const caller = { scope: "global", role: "operator", nativeSessionId: session.nativeSessionId,
30
- agentId: session.agentId, adapterId: session.adapterId };
29
+ const session = scope.kind === "task" ? activeLiveRoleAgentSession(tx.getGlobalRoleSessionSet("operator")) : null;
30
+ if (scope.kind === "task" && !session)
31
+ throw new Error("A current Operator Session is required for Task Job management.");
32
+ const caller = session ? { scope: "global", role: "operator", nativeSessionId: session.nativeSessionId,
33
+ agentId: session.agentId, adapterId: session.adapterId } : { scope: "user" };
31
34
  const { requestId, ...fields } = input;
35
+ if (typeof fields.reference === "string")
36
+ fields.reference = JSON.parse(fields.reference);
37
+ if (fields.offset !== undefined) {
38
+ if (!/^\d{1,9}$/u.test(String(fields.offset)))
39
+ throw new Error("Invalid artifact offset.");
40
+ fields.offset = Number(fields.offset);
41
+ }
32
42
  if (input.op === "run" && (typeof requestId !== "string" || !requestId.trim() || requestId.length > 200)) {
33
43
  throw new Error("Immediate execution requires a stable requestId.");
34
44
  }
@@ -36,6 +46,21 @@ export function createWebJobs(store, home) {
36
46
  ...(input.op === "run" ? { trigger: { source: "manual", key: requestId } } : {}) };
37
47
  return (family === "jobs" ? createJobDefinitionControl(tx, home) : createScheduleControl(tx, home))(params, now);
38
48
  };
49
+ // Artifact Git I/O is asynchronous, with its own authorization recheck and
50
+ // short publication transaction. Never keep a SQLite transaction across it.
51
+ if (family === "jobs" && value && typeof value === "object"
52
+ && "op" in value && String(value.op).startsWith("artifact-")) {
53
+ try {
54
+ return Promise.resolve(apply(store)).catch(error => {
55
+ if (!write)
56
+ throw new WebRequestRejected(error instanceof Error ? error.message : String(error));
57
+ throw error; // A Git/publication failure is not proof of non-submission.
58
+ });
59
+ }
60
+ catch (error) {
61
+ throw new WebRequestRejected(error instanceof Error ? error.message : String(error));
62
+ }
63
+ }
39
64
  if (write)
40
65
  return webLocalMutation(store, apply);
41
66
  try {
@@ -30,24 +30,36 @@ async function fixedImage(home, taskId, input) {
30
30
  /** One selected immutable file, never a workspace path or a remote URL.
31
31
  * Markdown is bounded and whole so fences/tables are not split by pagination. */
32
32
  export async function readArtifactPreview(home, taskId, ref, offset = 0) {
33
- if (/\.(svg|gif|avif|bmp|ico|tiff?)$/iu.test(ref.relativePath)) {
33
+ if (ref.taskId !== taskId)
34
+ throw new WebRequestRejected("Materials must belong to the same Task.");
35
+ const valid = validateGitArtifactRef(ref);
36
+ const file = await resolveGitArtifact(home, valid);
37
+ return { ...valid, ...previewArtifactContent(file, offset) };
38
+ }
39
+ /** Shared data-only preview for Task and Job evidence; no path lookup or authority. */
40
+ export function previewArtifactContent(file, offset = 0) {
41
+ if (/\.(svg|gif|avif|bmp|ico|tiff?)$/iu.test(file.relativePath)) {
34
42
  throw new WebRequestRejected("Unsupported image format. Save a static PNG, JPEG or WebP instead.");
35
43
  }
36
- if (isRasterArtifact(ref.relativePath)) {
44
+ if (file.relativePath.length > 512)
45
+ throw new WebRequestRejected("Artifact path is too long.");
46
+ if (isRasterArtifact(file.relativePath)) {
37
47
  if (offset !== 0)
38
48
  throw new WebRequestRejected("Images do not have text offsets.");
39
- return fixedImage(home, taskId, ref);
49
+ return { digest: file.digest, kind: "image", byteSize: file.bytes.length,
50
+ ...rasterMetadata(file.bytes), base64: file.bytes.toString("base64") };
40
51
  }
41
- if (/\.md$|\.markdown$/iu.test(ref.relativePath)) {
52
+ textPath(file.relativePath);
53
+ const text = decodeText(file.bytes);
54
+ if (/\.md$|\.markdown$/iu.test(file.relativePath)) {
42
55
  if (offset !== 0)
43
56
  throw new WebRequestRejected("Markdown previews start at offset zero.");
44
- const file = await fixedText(home, taskId, ref);
45
- if (file.bytes > MAX_MATERIAL_BYTES)
57
+ if (file.bytes.length > MAX_MATERIAL_BYTES)
46
58
  throw new WebRequestRejected("Markdown preview exceeds 256 KiB; split the document.");
47
- return { ...file.ref, kind: "markdown", byteSize: file.bytes, content: file.text,
48
- offset: 0, totalCharacters: file.text.length, nextOffset: null };
59
+ return { digest: file.digest, kind: "markdown", byteSize: file.bytes.length, content: text,
60
+ offset: 0, totalCharacters: text.length, nextOffset: null };
49
61
  }
50
- return readTextArtifactPage(home, taskId, ref, offset);
62
+ return { digest: file.digest, byteSize: file.bytes.length, ...page(text, offset) };
51
63
  }
52
64
  function textPath(path) {
53
65
  const safe = safeRelativeArtifactPath(path);
@@ -284,15 +284,16 @@ async function handleHttpRequest(request, response, store, dependencies, token,
284
284
  let value;
285
285
  if (method === "GET") {
286
286
  const query = new URL(request.url, "http://localhost").searchParams;
287
- if ([...query.keys()].some(key => !["op", "id", "runId"].includes(key)))
288
- throw new WebRequestRejected("Unknown Job query.");
289
- value = dependencies.jobs.read(scope, family, Object.fromEntries(query), now());
287
+ if ([...query.keys()].some(key => !["op", "id", "runId", "reference", "offset"].includes(key)
288
+ || query.getAll(key).length !== 1))
289
+ throw new WebRequestRejected("Unknown or repeated Job query.");
290
+ value = await dependencies.jobs.read(scope, family, Object.fromEntries(query), now());
290
291
  }
291
292
  else if (method === "POST") {
292
293
  if (headerValue(request, "origin") !== `http://${headerValue(request, "host")}`) {
293
294
  throw new WebRequestRejected("Job changes require a same-origin request.");
294
295
  }
295
- value = dependencies.jobs.mutate(scope, family, await readMutationBody(request), now());
296
+ value = await dependencies.jobs.mutate(scope, family, await readMutationBody(request), now());
296
297
  }
297
298
  else
298
299
  throw new WebRequestRejected("Method not allowed.");
@@ -631,7 +632,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
631
632
  try {
632
633
  const assetVersion = new URL(request.url, "http://localhost").searchParams.get("v");
633
634
  const asset = findWebAsset(pathname, assetVersion);
634
- const managementPage = /^\/(?:models(?:\/(?:providers|catalog|quota))?|settings(?:\/(?:preferences|skills|server|connection))?)\/?$/.test(pathname);
635
+ const managementPage = /^\/(?:models(?:\/(?:providers|catalog|quota))?|settings(?:\/(?:preferences|skills|jobs|server|connection))?)\/?$/.test(pathname);
635
636
  if (pathname === "/" || pathname === "/index.html" || managementPage) {
636
637
  sendText(response, 200, "text/html; charset=utf-8", prepareWebDocument(managementPage ? MODELS_HTML.replace("__YUI_WEB_TOKEN__", token) : dashboardHtml(token), managementPage ? "/assets/js/app/manage.js" : "/assets/app.js"), method === "HEAD");
637
638
  }
@@ -53,6 +53,7 @@ export function createWebTaskSurface(store, options = {}, observations = [], hos
53
53
  core: readTaskContext(tx, taskId, environment),
54
54
  task: {
55
55
  id: task.id, title: clip(task.title), status: task.status, updatedAt: task.updatedAt,
56
+ source: task.source,
56
57
  executionGate: task.executionGate, priority: task.priority, tags: task.tags?.slice(0, 16).map(value => clip(value)),
57
58
  projectBindings: task.projectBindings.slice(0, 32),
58
59
  description: clip(task.description), completionSummary: clip(task.completionSummary),
@@ -0,0 +1,130 @@
1
+ # Caller identity, discovery and authorization
2
+
3
+ Yui is a single-user local control plane, not a security boundary against another
4
+ process that can modify the same user's files. A provider thread ID is a location
5
+ hint, not proof that Yui manages that thread. Ordinary unmanaged Agents share the
6
+ existing public-user CLI surface; they do not need an Operator registration,
7
+ Manifest or per-thread Grant. This changes classification, not the user's
8
+ authorization for any external effect.
9
+
10
+ ## Responsibilities and sources
11
+
12
+ | Question | Authoritative implementation | What it does not establish |
13
+ | --- | --- | --- |
14
+ | Which command templates should this audience see? | `cli/commandCatalog.ts`, projected by `commandDiscovery.ts` into help and completion | Current authentication, target authorization or a Grant |
15
+ | Is this native identity ordinary, current or historical? | `runtime/sessionCaller.ts`, existing RoleSessionSet `sessions` and `history`; `storage/nativeSessionLookup.ts` | A new identity register or cross-Home authentication |
16
+ | May this Session act now? | `runtime/managedCaller.ts`, current Role binding, revocation and captured effective launch | Active Run occupancy is not required for ordinary Leader management |
17
+ | Which Task, Assignment, Project and records may it use? | `task/taskAuthority.ts`, context read authorization and command domain owners | A general “query is safe” rule |
18
+ | Which capability template and exact target are available? | `kernel/builtinCapabilities.ts`, `CapabilityRegistry`, domain `AccessAssessment` | Provider availability or a reusable authorization token |
19
+ | May a Controller operation execute? | `CallAuthority`, `jobControl` and the corresponding service | CLI help is not an access-control layer |
20
+ | May it access an additional resource or irreversible effect? | `CapabilityGrant`, `grant/taskAuthorization.ts`, resource/plugin/release consumers | Task activation does not authorize arbitrary resources |
21
+
22
+ The full command inventory is generated from the catalog, not maintained as a
23
+ second per-command permissions table: use `yui --json help` and scoped
24
+ `yui --json help task`, or traverse `describeCommandTree(discoveryCommandTree(env))`
25
+ in offline tooling. Complete role hints retain an offline template even without
26
+ a running Home; that is not authentication. Native-ID-only callers are resolved
27
+ from read-only local storage for help/completion; if that read fails the CLI shows
28
+ the restricted `unbound` template. No Controller or provider is started for that
29
+ observation. Execution resolves the original caller again and performs the
30
+ applicable domain checks.
31
+
32
+ ## Caller matrix
33
+
34
+ | Caller | Default rights | Boundaries / escalation |
35
+ | --- | --- | --- |
36
+ | User terminal; unknown valid `CODEX_THREAD_ID` with no Yui hints | Existing public catalog: public Project/config/Task information, Draft creation, user participation and existing public maintenance entry points | No managed inbox, Job or Grant issuer identity; no automatic discussion/development. Only perform effects the user actually requested |
37
+ | Current Operator (including native-ID-only Desktop) | Existing global management and Task supervision; explicit Grant issuance/revocation | Current native binding and operation-specific resource/effect checks |
38
+ | Custom GlobalRole | Its visible read/self-context and configured scoped operations | Not an Operator merely because global; Home mutations and Task-management rights are not inherited from Operator |
39
+ | Task Leader | Own Task management and readable context; delivery work within captured writable Projects | Planning Sessions do not gain delivery authority from Task activation. New resource/effect authority uses an original user/Operator source or InputRequest |
40
+ | Worker / Reviewer, including native-ID-only | Current Assignment's selected context, workspace and operations | Frozen references, Assignment/Review identity and Project bounds; ask Leader via the existing Assignment Message for missing authority |
41
+ | Partial, conflicting or unknown **Yui** identity; historical/replaced/stopped Session | No public fallback | Use original Home and current Session; preserve original input and receipts. Explicitly scoped historical self-diagnostics remain available through their existing entry |
42
+ | Revoked Session | No revived mutation/inbox authority | Command-time revocation checks remain authoritative even if an offline template is visible |
43
+
44
+ Public is not synonymous with read-only: terminal users already have maintenance
45
+ and participation commands. This change intentionally gives ordinary Agents that
46
+ same product surface, not all internal commands. Conversely, a read may expose
47
+ private Assignment context, consume a resource Grant or acquire a provider:
48
+ never derive permission from command spelling or the `query` effect alone.
49
+
50
+ Draft creation copies the configured Leader Agent, model, permissions and profile
51
+ through the existing role-copy path. It saves facts without creating a Run,
52
+ Session or execution workspace. `discuss` and `develop` remain separate explicit
53
+ submission/activation paths.
54
+
55
+ ## Discovery's three states
56
+
57
+ | Observation | Meaning | Execution behavior |
58
+ | --- | --- | --- |
59
+ | `hidden` | Caller cannot discover/apply for that capability/target | Omitted/unavailable; cannot be enabled by typing a hidden help command |
60
+ | `requestable` | Existing request route can supply the missing exact authority | Includes action, Task, parameter bounds and `leader-message`, `leader-input` or `operator` route; call is denied until authority exists |
61
+ | `authorized` | Current caller and exact parameters satisfy the assessed boundary | Call rechecks current state; revocation, expiry, consumed uses or provider failure can still prevent execution |
62
+ | No `access` field | Template or domain without an exact assessment | Not a fourth persisted state and not a claim of authorization; supply target input where supported and obey domain checks |
63
+
64
+ Capability search/commands/panels reuse the same registry and assessments.
65
+ Describe with exact input is an observation only: it does not acquire a provider
66
+ or consume a Grant. Hidden/denied, invalid input and provider unavailability remain
67
+ distinct. Static help/completion cannot promise any of these exact target states.
68
+
69
+ ## Grant and delegation matrix
70
+
71
+ | Capability / effect | Default or reusable authority | Required exact checks |
72
+ | --- | --- | --- |
73
+ | Public information | Existing public command path, no Grant | Local Home/storage contract and domain visibility |
74
+ | Task / Assignment reads and management | Current matching Role; permission categories such as `task:read`, `task:manage` are reused | Task/Assignment/Review identity, record readability, lifecycle, planning/delivery |
75
+ | Job operations | Current managed caller, no repeated per-command approval inside authorized work | Controller reauthentication, owner, workspace, project, head and Assignment |
76
+ | Local resource read/write | Existing resource registration plus exact Grant when required | Task, canonical resource path/id, read vs write, parameter bounds, expiry/use count |
77
+ | Plugin management vs execution | Authorized management is separate from executing author code | Adopted environment, plugin/package digest, phase and exact `plugin.execute` Grant |
78
+ | Release / other high-impact effects | Explicit user authority; existing Operator or source-authorized delivery-Leader Grant | Project/repository/commit, action, package/version/registry where applicable, irreversible ceiling and original operation receipts |
79
+
80
+ Operator issuance uses its current managed Session. Delivery-Leader issuance must
81
+ cite an original user/Operator Message and exact quoted purpose, stays inside the
82
+ Task and selected Projects, requires finite uses/expiry and action-specific bounds.
83
+ A Role report is not user authority. Worker/Reviewer requests go to Leader; a
84
+ genuinely missing user decision uses InputRequest. A response is not itself a
85
+ Grant. Consumption, revocation and idempotent retries remain with existing
86
+ consumers; discovery does not replenish uses or turn unknown effects into retries.
87
+ No extra approval is added for already-authorized normal role work.
88
+
89
+ ## Identity limits and recovery
90
+
91
+ The canonical lookup includes both active and historical registrations, including
92
+ inactive Agents in a Session set. The active runtime inventory remains optimized
93
+ for scheduling; it is deliberately not used to prove absence of managed history.
94
+ Unbinding moves a stopped Session into the existing history rather than deleting
95
+ its identity. Role removal retains stopped Session sets while deleting the Role
96
+ and active runtime projection; those records do not grant authority without a
97
+ current Role binding. History erased by an earlier version cannot be reconstructed.
98
+ Storage 1.13 → 1.14 removes only the global Session table's Role foreign key so
99
+ that its existing identity evidence may outlive Role removal; payload bytes are
100
+ preserved. Existing Homes require an explicit supported upgrade, never an
101
+ implicit read-time migration. There is no new authorization state, global thread
102
+ registry or cross-Home scan.
103
+
104
+ An unknown provider ID in Home B contains no evidence that it was managed in Home
105
+ A. Treating every such ID as invalid would again reject ordinary Agent sessions.
106
+ Therefore retain the intended Home/Manifest/Yui hints for managed work; with those
107
+ hints, a missing registration fails closed. Do not clear hints, forge a Role,
108
+ borrow a parent's transport identity or change Home to obtain public authority.
109
+ Ambiguous registration also fails closed. Native-ID-only help on broken storage
110
+ remains a limited template, while execution reports the original storage/identity
111
+ failure. Complete launch hints can still show a role's static help on a broken
112
+ Home; they cannot authorize reads or effects.
113
+
114
+ ## Verification and maintenance
115
+
116
+ `cli-authority.test.js` exercises isolated launcher-backed public reads, Draft
117
+ creation/config inheritance, native-only managed callers, historical denial,
118
+ maintenance and cross-Task fences. `agent-inbox.test.js` covers current/historical
119
+ and ambiguous native lookup plus Assignment/Reviewer/revocation boundaries.
120
+ `caller-discovery.test.js` covers projection and transient three-state behavior;
121
+ `leader-authorization.test.js` covers real registry/resource/Grant issuance,
122
+ consumption, revocation and surfaces. Controller Job checks are tested independently
123
+ of CLI discovery. Run the required `npm test` gate; no real model or production
124
+ Home is needed for these contracts.
125
+
126
+ When adding a command, extend the catalog for presentation and the actual domain
127
+ owner for authorization; do not invent a second CLI allowlist. When adding a
128
+ capability, reuse the registry permission category and its resource owner's
129
+ assessment/consumption checks. Extend a category only when the new operation
130
+ really has the same data and effect boundary.