@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
@@ -2,38 +2,74 @@
2
2
  * Controlled Project domain action invoked by the artifact-only Job runner.
3
3
  * Its catalog connection is read-only; only the shared domain service writes Git.
4
4
  */
5
- import { resolve } from "node:path";
5
+ import { join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { SqliteTaskStore } from "../storage/sqliteStore.js";
8
8
  import { authorizeGlobalJobStart } from "../controller/jobDefinitionControl.js";
9
9
  import { refreshRegisteredProject } from "../repository/projectRefresh.js";
10
10
  import { projectRefreshDigest } from "./builtinJob.js";
11
11
  import { CliError } from "../errors/cliError.js";
12
+ import { writeTextFileAtomically } from "../storage/durableFile.js";
13
+ import { runReference } from "./jobBusinessResult.js";
14
+ import { requireProjectSyncCheckout } from "./projectSync.js";
12
15
  export async function runProjectRefresh(home, runId) {
13
16
  const store = new SqliteTaskStore(home, { readonly: true });
14
17
  try {
15
18
  const run = store.getGlobalJobRun(runId);
16
- if (!run || run.action !== "project.refresh" || !["queued", "running"].includes(run.status)
17
- || run.operation.effect !== "possible" || run.cancelRequestedAt) {
19
+ if (!run || run.action !== "project.refresh"
20
+ || !((run.status === "queued" && run.operation.effect === "possible")
21
+ || (run.status === "running" && run.operation.effect === "confirmed"))
22
+ || run.cancelRequestedAt) {
18
23
  throw new Error("No active admitted project.refresh Run.");
19
24
  }
20
- authorizeGlobalJobStart(store, run);
21
- const result = await refreshRegisteredProject(store, run.projectId, {
22
- automatic: true,
23
- expectedDigest: projectRefreshDigest({
24
- kind: "builtin", action: run.action, projectId: run.projectId, input: run.actionInput
25
- }),
26
- beforeRefresh: () => {
27
- const current = store.getGlobalJobRun(runId);
28
- if (!current || !["queued", "running"].includes(current.status) || current.cancelRequestedAt) {
29
- throw new Error("Project refresh Run stopped while waiting for maintenance.");
25
+ let result;
26
+ try {
27
+ // A revoked/stale grant before Git is a known refusal. Persist its
28
+ // business receipt just like a refusal after waiting for maintenance.
29
+ authorizeGlobalJobStart(store, run);
30
+ result = await refreshRegisteredProject(store, run.projectId, {
31
+ automatic: true,
32
+ expectedDigest: projectRefreshDigest({
33
+ kind: "builtin", action: run.action, projectId: run.projectId, input: run.actionInput
34
+ }),
35
+ beforeRefresh: () => {
36
+ const current = store.getGlobalJobRun(runId);
37
+ if (!current || !["queued", "running"].includes(current.status) || current.cancelRequestedAt) {
38
+ throw new Error("Project refresh Run stopped while waiting for maintenance.");
39
+ }
40
+ authorizeGlobalJobStart(store, current);
41
+ const job = store.getJobDefinition({ kind: "global" }, current.definitionId);
42
+ if (job?.builtin)
43
+ requireProjectSyncCheckout(store, job);
30
44
  }
31
- authorizeGlobalJobStart(store, current);
32
- }
33
- });
34
- process.stdout.write(JSON.stringify({ action: run.action, projectId: run.projectId,
45
+ });
46
+ }
47
+ catch (error) {
48
+ const details = error instanceof CliError ? error.details : {};
49
+ const partial = details.refresh;
50
+ const business = {
51
+ schemaVersion: 1, sourceRun: runReference(run),
52
+ outcome: partial?.changed === null ? "unknown-needs-attention" : "failed",
53
+ summary: error instanceof Error ? error.message : String(error),
54
+ data: JSON.parse(JSON.stringify({ action: run.action, projectId: run.projectId,
55
+ target: run.actionInput, observedAt: new Date().toISOString(), details })),
56
+ evidence: []
57
+ };
58
+ writeTextFileAtomically(join(home, run.artifactsLocator, "business-result.json"), JSON.stringify(business) + "\n");
59
+ throw error;
60
+ }
61
+ const data = { action: run.action, projectId: run.projectId,
35
62
  trigger: run.trigger, outcome: "succeeded", observedAt: new Date().toISOString(),
36
- remoteCheck: { status: "verified", commit: result.toCommit }, refresh: result }) + "\n");
63
+ remoteCheck: { status: "verified", commit: result.toCommit }, refresh: result };
64
+ const business = {
65
+ schemaVersion: 1, sourceRun: runReference(run), outcome: "succeeded",
66
+ summary: `Project ${run.projectId} refreshed to ${result.toCommit}.`,
67
+ data: JSON.parse(JSON.stringify(data)), evidence: []
68
+ };
69
+ // Persist separately from stdout. If this write fails after Git effects,
70
+ // the collector reports unknown, never a safely retryable failed action.
71
+ writeTextFileAtomically(join(home, run.artifactsLocator, "business-result.json"), JSON.stringify(business) + "\n");
72
+ process.stdout.write(JSON.stringify(data) + "\n");
37
73
  }
38
74
  finally {
39
75
  store.close();
@@ -0,0 +1,162 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstatSync, realpathSync } from "node:fs";
3
+ import { isDeepStrictEqual } from "node:util";
4
+ import { join, resolve } from "node:path";
5
+ import { managedProjectPath } from "../repository/project.js";
6
+ import { requireRefreshProject, projectRefreshTargetDigest } from "../repository/projectRefresh.js";
7
+ import { createJobDefinition, reviseJobDefinition, validateJobDefinition } from "./jobDefinition.js";
8
+ import { nextScheduleDue, validateScheduleRule } from "../schedule/schedule.js";
9
+ import { scheduleServiceBinding } from "./scheduleService.js";
10
+ const scope = { kind: "global" };
11
+ const unavailable = "Project sync unavailable: ";
12
+ export const PROJECT_SYNC_INTERVAL_MS = 6 * 60 * 60_000;
13
+ /** Called under the maintenance lock immediately before Git, not a timer probe. */
14
+ export function requireProjectSyncCheckout(store, job) {
15
+ requireProjectSyncTarget(store, job);
16
+ const path = managedProjectPath(store.rootDirectory(), job.configuration.kind === "builtin" ? job.configuration.projectId : "");
17
+ if (realpathSync(path) !== path || !lstatSync(join(path, ".git")).isDirectory()
18
+ || lstatSync(join(path, ".git")).isSymbolicLink()) {
19
+ throw new Error("Builtin sync requires its own canonical checkout, not a linked or shared worktree.");
20
+ }
21
+ }
22
+ /** Current registry and maintenance source, not the builtin label, bound authority. */
23
+ export function requireProjectSyncTarget(store, job) {
24
+ if (job.builtin !== "project-sync" || job.configuration.kind !== "builtin")
25
+ throw new Error("Not a builtin Project sync.");
26
+ validateJobDefinition(job);
27
+ const project = requireRefreshProject(store, job.configuration.projectId, true);
28
+ if (resolve(project.path) !== managedProjectPath(store.rootDirectory(), project.id)) {
29
+ throw new Error("Project sync requires its canonical managed checkout.");
30
+ }
31
+ if (!isDeepStrictEqual(job.configuration.input, { targetDigest: projectRefreshTargetDigest(project) })) {
32
+ throw new Error("Project sync target changed; await registry reconciliation.");
33
+ }
34
+ const custom = store.listJobDefinitions(scope).find(other => !other.builtin && !other.instantiation
35
+ && other.configuration.kind === "builtin" && other.configuration.action === "project.refresh"
36
+ && other.configuration.projectId === project.id);
37
+ if (custom)
38
+ throw new Error(`Existing custom maintenance Job ${custom.id}; builtin sync does not take it over. Explicitly retarget that Job before enabling builtin sync.`);
39
+ }
40
+ function grant(store, plan, job, now) {
41
+ requireProjectSyncTarget(store, job);
42
+ const grantId = randomUUID();
43
+ return { kind: "service", service: "schedule", grantId, revoked: false, jobRevision: job.revision,
44
+ grantedBy: { scope: "service", service: "project-sync" }, grantedAt: now.toISOString(),
45
+ binding: scheduleServiceBinding(store, plan, job, grantId) };
46
+ }
47
+ /** Called by the existing Controller Schedule pass, with no timer or Git effects. */
48
+ export function reconcileProjectSync(store, now, onError = () => { }) {
49
+ const ids = new Set([...store.listProjects().map(p => p.id),
50
+ ...store.listSchedules(scope).flatMap(s => s.builtin ? [s.builtin.projectId] : [])]);
51
+ for (const projectId of ids) {
52
+ try {
53
+ store.transaction(tx => {
54
+ const id = `project-sync-${projectId}`;
55
+ const project = tx.listProjects().find(p => p.id === projectId);
56
+ let job = tx.getJobDefinition(scope, id);
57
+ let plan = tx.getSchedule(scope, id);
58
+ if ((job && job.builtin !== "project-sync") || (plan && !plan.builtin)) {
59
+ throw new Error(`Reserved builtin identity ${id} is occupied; existing records were not adopted.`);
60
+ }
61
+ if (!job && !project)
62
+ return;
63
+ let changed = false;
64
+ if (project) {
65
+ const configuration = { kind: "builtin", action: "project.refresh", projectId,
66
+ input: { targetDigest: projectRefreshTargetDigest(project) } };
67
+ if (!job) {
68
+ job = validateJobDefinition({ ...createJobDefinition({ id, scope, title: `Project sync · ${project.name}`, configuration }, now),
69
+ builtin: "project-sync" });
70
+ tx.saveJobDefinition(job);
71
+ changed = true;
72
+ }
73
+ else if (!isDeepStrictEqual(job.configuration, configuration)) {
74
+ job = reviseJobDefinition(job, { title: job.title, configuration }, now);
75
+ tx.saveJobDefinition(job);
76
+ changed = true;
77
+ }
78
+ }
79
+ if (!job)
80
+ throw new Error(`Builtin Project sync definition missing: ${id}.`);
81
+ let reason;
82
+ try {
83
+ requireProjectSyncTarget(tx, job);
84
+ }
85
+ catch (error) {
86
+ reason = unavailable + (error instanceof Error ? error.message : String(error));
87
+ }
88
+ if (!plan) {
89
+ const rule = { kind: "interval", anchor: new Date(now.getTime() + PROJECT_SYNC_INTERVAL_MS).toISOString(),
90
+ everyMs: PROJECT_SYNC_INTERVAL_MS };
91
+ const grantId = randomUUID();
92
+ plan = { schemaVersion: 1, id, scope, jobId: id, target: { kind: "existing-job" }, rule,
93
+ builtin: { projectId, enabled: true }, revision: 1, generation: 1,
94
+ state: reason ? "needs-attention" : "enabled", nextDue: nextScheduleDue(rule, now),
95
+ authorization: { kind: "service", service: "schedule", grantId, revoked: true, jobRevision: job.revision,
96
+ grantedBy: { scope: "service", service: "project-sync" }, grantedAt: now.toISOString(),
97
+ binding: scheduleServiceBinding(tx, { id, scope, jobId: id, target: { kind: "existing-job" } }, job, grantId) },
98
+ ...(reason ? { reason } : {}), createdAt: now.toISOString(), updatedAt: now.toISOString() };
99
+ if (!reason)
100
+ plan = { ...plan, authorization: grant(tx, plan, job, now) };
101
+ tx.saveSchedule(plan);
102
+ return;
103
+ }
104
+ const original = plan;
105
+ if (plan.authorization.kind !== "service")
106
+ throw new Error("Builtin Schedule requires service authority.");
107
+ if (reason || !plan.builtin.enabled) {
108
+ plan = { ...plan, state: plan.builtin.enabled ? "needs-attention" : "disabled",
109
+ reason: reason ?? "Project sync disabled by user.",
110
+ authorization: { ...plan.authorization, revoked: true } };
111
+ }
112
+ else if (changed || plan.reason?.startsWith(unavailable)) {
113
+ const { reason: _reason, ...rest } = plan;
114
+ const after = new Date(Math.max(now.getTime(), Date.parse(plan.processedThrough ?? "") || 0));
115
+ plan = { ...rest, state: "enabled", nextDue: nextScheduleDue(plan.rule, after),
116
+ processedThrough: after.toISOString(), generation: plan.generation + 1,
117
+ authorization: grant(tx, plan, job, now) };
118
+ }
119
+ // Unchanged dispatch failures are visible, not automatically retried/re-authorized.
120
+ if (!isDeepStrictEqual(original, plan))
121
+ tx.saveSchedule({
122
+ ...plan, revision: original.revision + 1, updatedAt: now.toISOString()
123
+ });
124
+ });
125
+ }
126
+ catch (error) {
127
+ onError(error instanceof Error ? error : new Error(String(error)));
128
+ }
129
+ }
130
+ }
131
+ /** Narrow user override. No alternative writable target or authorization surface. */
132
+ export function updateProjectSync(store, previous, op, rule, now) {
133
+ if (!previous.builtin || previous.authorization.kind !== "service")
134
+ throw new Error("Not a builtin Schedule.");
135
+ if (!["enable", "disable", "revoke", "update"].includes(op))
136
+ throw new Error("Unsupported builtin Schedule operation.");
137
+ const nextRule = rule ?? previous.rule;
138
+ validateScheduleRule(nextRule);
139
+ if (nextRule.kind === "once" || (nextRule.kind === "interval" && nextRule.everyMs < 60_000)) {
140
+ throw new Error("Builtin sync requires a recurring rule (interval at least 60000ms, or five-field cron).");
141
+ }
142
+ const enabled = op === "enable" ? true : ["disable", "revoke"].includes(op) ? false : previous.builtin.enabled;
143
+ const after = new Date(Math.max(now.getTime(), Date.parse(previous.processedThrough ?? "") || 0));
144
+ const { reason: _reason, ...rest } = previous;
145
+ let next = { ...rest, builtin: { ...previous.builtin, enabled }, rule: structuredClone(nextRule),
146
+ state: enabled ? "enabled" : "disabled", revision: previous.revision + 1,
147
+ generation: previous.generation + (op === "update" ? 1 : 0), updatedAt: now.toISOString(),
148
+ nextDue: nextScheduleDue(nextRule, after), processedThrough: after.toISOString() };
149
+ if (!enabled)
150
+ next = { ...next, reason: "Project sync disabled by user.",
151
+ authorization: { ...previous.authorization, revoked: true } };
152
+ else {
153
+ const job = store.getJobDefinition(scope, previous.jobId);
154
+ if (!job)
155
+ throw new Error("Builtin definition missing.");
156
+ requireProjectSyncTarget(store, job);
157
+ next = { ...next, authorization: previous.authorization.revoked || op === "enable"
158
+ ? grant(store, next, job, now) : previous.authorization };
159
+ }
160
+ store.saveSchedule(next);
161
+ return next;
162
+ }
@@ -0,0 +1,55 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import { requireProjectSyncTarget } from "./projectSync.js";
4
+ /** Internal compiled registration, not a caller identity accepted by RPC. */
5
+ const actions = new Set(["system.noop", "project.refresh"]);
6
+ export function requireServiceTarget(job) {
7
+ if (job.scope.kind !== "global" || job.configuration.kind !== "builtin"
8
+ || !actions.has(job.configuration.action))
9
+ throw new Error("Schedule service is not registered for this scope/action.");
10
+ }
11
+ export function scheduleServiceBinding(store, schedule, job, grantId) {
12
+ requireServiceTarget(job);
13
+ return createHash("sha256").update(JSON.stringify([
14
+ "schedule-service/v1", store.getHomeIdentity().homeId, grantId,
15
+ schedule.id, schedule.scope, schedule.jobId, schedule.target, job
16
+ ])).digest("hex");
17
+ }
18
+ export function requireScheduleService(store, ref) {
19
+ const schedule = store.getSchedule({ kind: "global" }, ref.scheduleId);
20
+ const auth = schedule?.authorization;
21
+ if (!schedule || auth?.kind !== "service" || auth.revoked || auth.grantId !== ref.grantId) {
22
+ throw new Error("Schedule service authority was revoked; no execution was started.");
23
+ }
24
+ const source = store.getJobDefinition(schedule.scope, schedule.jobId);
25
+ if (!source || source.revision !== auth.jobRevision
26
+ || scheduleServiceBinding(store, schedule, source, auth.grantId) !== auth.binding) {
27
+ throw new Error("Schedule service target or authority changed; explicitly reauthorize.");
28
+ }
29
+ if (schedule.builtin) {
30
+ if (!schedule.builtin.enabled)
31
+ throw new Error("Builtin Project sync was disabled.");
32
+ requireProjectSyncTarget(store, source);
33
+ }
34
+ return schedule;
35
+ }
36
+ /** Bind each immutable Run to its exact stored grant, definition and input. */
37
+ export function serviceRunBinding(store, run, schedule) {
38
+ return createHash("sha256").update(JSON.stringify([
39
+ "schedule-service-run/v1", store.getHomeIdentity().homeId, schedule.authorization.binding,
40
+ run.serviceAuthorization, run.id, run.definitionId, run.definitionRevision,
41
+ run.action, run.projectId, run.actionInput, run.trigger, run.operation.requestId
42
+ ])).digest("hex");
43
+ }
44
+ export function authorizeServiceJob(store, job, ref, key) {
45
+ const schedule = requireScheduleService(store, ref);
46
+ const source = store.getJobDefinition(schedule.scope, schedule.jobId);
47
+ const matches = schedule.target.kind === "existing-job" ? job.id === source.id
48
+ : job.instantiation?.sourceId === source.id && job.instantiation.sourceRevision === source.revision
49
+ && job.instantiation.key === key;
50
+ if (!matches || !key.startsWith(`${schedule.id}:`)
51
+ || !isDeepStrictEqual(job.configuration, source.configuration)) {
52
+ throw new Error("Job is outside the Schedule service authority target.");
53
+ }
54
+ return schedule;
55
+ }
@@ -0,0 +1,48 @@
1
+ import { requireIdentity, requireKnownFields, requireText } from "../domain/validation.js";
2
+ import { createTask, validateTask } from "../task/task.js";
3
+ import { createHash } from "node:crypto";
4
+ /** WorkItem revision also advances on execution/candidate submission. Freeze
5
+ * requirements, not that mutable progress counter, as the delegated scope. */
6
+ export function workItemDefinitionDigest(work) {
7
+ return createHash("sha256").update(JSON.stringify([work.title, work.objective, work.acceptance,
8
+ work.dependsOn, work.writeProjectIds, work.baseRefs ?? null])).digest("hex");
9
+ }
10
+ export function validateTaskJobConfiguration(config) {
11
+ requireKnownFields(config, ["kind", "goal", "target"], "Task Job");
12
+ if (config.target.kind === "new") {
13
+ requireKnownFields(config.target, ["kind", "title", "description", "type", "projectBindings"], "New Task target");
14
+ if (!["delivery", "task-created"].includes(config.goal))
15
+ throw new Error("New Task requires a delivery or explicit task-created goal.");
16
+ requireText(config.target.description, "Task requirements");
17
+ if (!Array.isArray(config.target.projectBindings))
18
+ throw new Error("Explicit Task Project bindings required.");
19
+ for (const binding of config.target.projectBindings) {
20
+ requireKnownFields(binding, ["projectId", "directory", "baseRef"], "Draft Project binding");
21
+ }
22
+ validateTask(createTask("task-1", config.target.title, new Date(0), config.target));
23
+ }
24
+ else if (config.target.kind === "work-item" && config.goal === "work-item") {
25
+ requireKnownFields(config.target, ["kind", "taskId", "workItemId", "revision"], "WorkItem target");
26
+ requireIdentity(config.target.taskId, "Target Task");
27
+ requireIdentity(config.target.workItemId, "Target WorkItem");
28
+ if (!/^task-\d+$/u.test(config.target.taskId) || !/^work-item-\d+$/u.test(config.target.workItemId)
29
+ || !Number.isSafeInteger(config.target.revision) || config.target.revision < 1)
30
+ throw new Error("Exact WorkItem scope and revision required.");
31
+ }
32
+ else
33
+ throw new Error("Invalid Task delegation target/goal.");
34
+ }
35
+ export function validateTaskDelegation(value) {
36
+ requireKnownFields(value, ["taskId", "goal", "workItemId", "workItemRevision", "workItemDefinitionDigest"], "Task delegation");
37
+ if (!/^task-\d+$/u.test(value.taskId))
38
+ throw new Error("Invalid delegated Task.");
39
+ if (value.goal === "work-item") {
40
+ if (!/^work-item-\d+$/u.test(value.workItemId ?? "")
41
+ || !Number.isSafeInteger(value.workItemRevision) || value.workItemRevision < 1
42
+ || !/^[a-f0-9]{64}$/u.test(value.workItemDefinitionDigest ?? ""))
43
+ throw new Error("Exact delegated WorkItem required.");
44
+ }
45
+ else if (!["delivery", "task-created"].includes(value.goal) || value.workItemId !== undefined
46
+ || value.workItemRevision !== undefined || value.workItemDefinitionDigest !== undefined)
47
+ throw new Error("Invalid delegated goal.");
48
+ }
@@ -32,7 +32,7 @@ export function resolveMessageRecipient(store, taskId, roleName, scope) {
32
32
  /** Current Assignment authority, independent of eligibility to start a new Run.
33
33
  * A Reviewer still owns its frozen clarification when Task main advances. */
34
34
  export function messageAssignmentBlocker(store, message) {
35
- if (isTaskRecordRetired(store.listEvents(message.taskId), "message", message.id))
35
+ if (message.id !== undefined && isTaskRecordRetired(store.listEvents(message.taskId), "message", message.id))
36
36
  return "message-retired";
37
37
  const target = message.recipient;
38
38
  if (target?.ownerRunId === undefined)
@@ -308,7 +308,7 @@ export function createProjectResources(store, now = () => new Date()) {
308
308
  || (run.workspace && overlaps(run.workspace.root, value.directory.path))))
309
309
  || sessionSets.some((set) => [...Object.values(set.sessions), ...(set.history ?? [])].some((session) => session.status === "active" && references(session.effective.executionEnvironment)))
310
310
  || tx.listDurableJobs(taskId).some((job) => ["queued", "running", "unknown-needs-attention"].includes(job.status)
311
- && overlaps(job.workspace, value.directory.path))))
311
+ && !job.delegation && overlaps(job.workspace, value.directory.path))))
312
312
  throw new Error("Environment still has a live or unknown execution reference.");
313
313
  }
314
314
  if (value.directory?.ownership === "preparation") {
@@ -43,7 +43,7 @@ export function readResourceGcState(store) {
43
43
  }),
44
44
  ...reader.listDurableJobs(task.id).filter(job => job.status === "queued" || job.status === "running"
45
45
  || (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined))
46
- .map(job => job.workspace)
46
+ .flatMap(job => job.delegation ? [] : [job.workspace])
47
47
  ])
48
48
  };
49
49
  });
package/dist/role/role.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
+ import { requireKnownFields } from "../domain/validation.js";
3
+ import { jobScopeKey } from "../job/jobDefinition.js";
2
4
  import { validateExecutionEnvironmentSnapshot } from "../resources/projectResource.js";
3
5
  import { isAgentAdapterId } from "../agent/adapterCatalog.js";
4
6
  import { adapterIdForExecutionComponent, resolveAgentExecutionComponent } from "../agent/executionComponents.js";
5
7
  import { defaultRoleAgentConfig, resolveAgentAdapter } from "../executor/agentAdapter.js";
6
- import { validateRoleSessionSet } from "../executor/agentExecutor.js";
8
+ import { roleAgentSessionRef, validateRoleSessionSet } from "../executor/agentExecutor.js";
7
9
  /**
8
10
  * The Agent argument is the stored record, not a hand-picked pair of fields.
9
11
  * `component` is required for exactly that reason: an Agent registered as the
@@ -159,24 +161,22 @@ export function unbindRoleAgent(role, sessions, agentId, now) {
159
161
  const globalSessions = sessions.owner.scope === "global"
160
162
  ? sessions
161
163
  : null;
162
- const targetHistory = globalSessions === null
163
- ? []
164
- : Object.entries(globalSessions.history ?? {}).filter(([, session]) => (session.agentId === normalizedAgentId));
165
- const targetTaskHistory = (taskSessions?.history ?? []).filter((session) => (session.agentId === normalizedAgentId));
166
- if (targetSession !== undefined || targetHistory.length > 0 || targetTaskHistory.length > 0) {
164
+ if (targetSession !== undefined) {
167
165
  const remainingSessions = { ...sessions.sessions };
168
166
  delete remainingSessions[normalizedAgentId];
169
- const remainingHistory = globalSessions === null
167
+ // Unbinding removes execution selection, not identity evidence. Otherwise
168
+ // a stopped native-ID-only caller could later be mistaken for public.
169
+ const history = globalSessions === null
170
170
  ? undefined
171
- : Object.fromEntries(Object.entries(globalSessions.history ?? {}).filter(([, session]) => (session.agentId !== normalizedAgentId)));
172
- const remainingTaskHistory = taskSessions === null
171
+ : { ...globalSessions.history, [roleAgentSessionRef(targetSession)]: targetSession };
172
+ const taskHistory = taskSessions === null
173
173
  ? undefined
174
- : (taskSessions.history ?? []).filter((session) => (session.agentId !== normalizedAgentId));
174
+ : [...(taskSessions.history ?? []), targetSession];
175
175
  updatedSessions = validateRoleSessionSet({
176
176
  ...sessions,
177
177
  sessions: remainingSessions,
178
- ...(remainingHistory === undefined ? {} : { history: remainingHistory }),
179
- ...(remainingTaskHistory === undefined ? {} : { history: remainingTaskHistory }),
178
+ ...(history === undefined ? {} : { history }),
179
+ ...(taskHistory === undefined ? {} : { history: taskHistory }),
180
180
  updatedAt: now.toISOString()
181
181
  });
182
182
  }
@@ -191,6 +191,16 @@ export function unbindRoleAgent(role, sessions, agentId, now) {
191
191
  export function validateGlobalRole(role) {
192
192
  if (Object.hasOwn(role, "taskId"))
193
193
  throw new Error("Global Role cannot carry a Task identity.");
194
+ if (role.jobOwner !== undefined) {
195
+ requireKnownFields(role.jobOwner, ["scope", "jobId", "runId"], "Job Role owner");
196
+ requireKnownFields(role.jobOwner.scope, role.jobOwner.scope.kind === "task" ? ["kind", "taskId"] : ["kind"], "Job Role scope");
197
+ if (!["task", "global"].includes(role.jobOwner.scope.kind) || role.name === "operator")
198
+ throw new Error("Invalid Job Role owner.");
199
+ jobScopeKey(role.jobOwner.scope);
200
+ requireSafeIdentity(role.jobOwner.jobId, "Job Role owner id");
201
+ if (role.jobOwner.runId !== undefined)
202
+ requireSafeIdentity(role.jobOwner.runId, "Job Role Run id");
203
+ }
194
204
  return validateRoleOwner(role);
195
205
  }
196
206
  export function validateTaskRole(role) {
@@ -1,6 +1,8 @@
1
- /** Presence is not authentication. Even partial transport identity must not
2
- * silently acquire the authority of an unbound public caller. */
1
+ /** Yui-specific hints assert managed intent, not authentication. A provider's
2
+ * CODEX_THREAD_ID alone is only a lookup hint: resolve it in the selected Home
3
+ * before routing execution. Partial Yui identity must never become public. */
3
4
  export function hasManagedIdentity(environment) {
4
5
  return ["YUI_SESSION_SCOPE", "YUI_ROLE", "YUI_AGENT_ID", "YUI_NATIVE_SESSION_ID",
5
- "YUI_TASK_ID", "YUI_SESSION_MANIFEST", "CODEX_THREAD_ID"].some(key => environment[key] !== undefined);
6
+ "YUI_TASK_ID", "YUI_SESSION_MANIFEST", "YUI_ADAPTER_ID", "YUI_WORKSPACE"]
7
+ .some(key => environment[key] !== undefined);
6
8
  }
@@ -15,20 +15,19 @@ export function sessionCallerEnvironment(store, env) {
15
15
  if (!native || native.trim() !== native || native.includes("\0")) {
16
16
  throw new ManagedRuntimeDriftError("Managed native Session identity is invalid.");
17
17
  }
18
- // Reuse the existing derived active-Session inventory; no new identity index
19
- // or scan of every Task body/history is needed.
20
- const matches = store.listRuntimeSessionCandidates().filter(session => session.nativeSessionId === (session.adapterId === "codex" ? env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID : env.YUI_NATIVE_SESSION_ID));
18
+ const identifies = (session) => session.nativeSessionId === (session.adapterId === "codex" ? env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID : env.YUI_NATIVE_SESSION_ID);
19
+ const matches = store.findNativeSessionSets([env.CODEX_THREAD_ID, env.YUI_NATIVE_SESSION_ID].filter((id) => id !== undefined)).filter(set => [...Object.values(set.sessions), ...Object.values(set.history ?? {})].some(identifies));
20
+ if (!matches.length && !hasManagedIdentity(env))
21
+ return env;
21
22
  if (matches.length !== 1) {
22
23
  throw new ManagedRuntimeDriftError(matches.length
23
24
  ? "Native Session identity is ambiguous in this YUI_HOME; no caller authority was selected."
24
- : "Native Session is not registered in this YUI_HOME; refusing to infer public user authority.");
25
+ : "Managed native Session is not registered in this YUI_HOME. Use its original Home/current Session; do not clear identity hints.");
25
26
  }
26
- const match = matches[0];
27
- const set = match.owner.scope === "global" ? store.getGlobalRoleSessionSet(match.owner.roleName)
28
- : store.getTaskRoleSessionSet(match.owner.taskId, match.owner.roleName);
29
- const session = set?.sessions[match.agentId];
30
- if (set === null || session === undefined || session.nativeSessionId !== match.nativeSessionId) {
31
- throw new ManagedRuntimeDriftError("Native Session registration changed during caller resolution; read current state again.");
27
+ const set = matches[0];
28
+ const session = set.sessions[set.activeAgentId];
29
+ if (session === undefined || session.status !== "active" || !identifies(session)) {
30
+ throw new ManagedRuntimeDriftError("Managed native Session is no longer current in this YUI_HOME. Use the current Session; historical identity cannot become public.");
32
31
  }
33
32
  const hints = {
34
33
  YUI_SESSION_SCOPE: set.owner.scope, YUI_ROLE: set.owner.roleName,
@@ -1,6 +1,7 @@
1
1
  import { CronExpressionParser } from "cron-parser";
2
2
  import { requireKnownFields, requirePositiveInteger, requireTimestamp, requireText } from "../domain/validation.js";
3
3
  import { jobScopeKey } from "../job/jobDefinition.js";
4
+ import { parseDurableJobCaller } from "../controller/jobControl.js";
4
5
  export function scheduleId(id) {
5
6
  if (typeof id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u.test(id)) {
6
7
  throw new Error("An explicit safe Schedule/Job id is required.");
@@ -49,16 +50,25 @@ export function nextScheduleDue(rule, after) {
49
50
  return CronExpressionParser.parse(rule.expression, { currentDate: after, tz: rule.timezone }).next().toISOString();
50
51
  }
51
52
  export function validateSchedule(value) {
52
- requireKnownFields(value, ["schemaVersion", "id", "scope", "jobId", "rule", "revision", "generation",
53
- "state", "nextDue", "processedThrough", "authorization", "lastAttempt", "reason", "createdAt", "updatedAt"], "Schedule");
53
+ requireKnownFields(value, ["schemaVersion", "id", "scope", "jobId", "target", "rule", "revision", "generation",
54
+ "state", "nextDue", "processedThrough", "authorization", "builtin", "lastAttempt", "reason", "createdAt", "updatedAt"], "Schedule");
54
55
  if (value.schemaVersion !== 1)
55
56
  throw new Error("Invalid Schedule schema.");
56
57
  scheduleId(value.id);
57
58
  scheduleId(value.jobId);
59
+ validateScheduleTarget(value.target);
58
60
  if (!["task", "global"].includes(value.scope.kind))
59
61
  throw new Error("Invalid Schedule scope.");
60
62
  requireKnownFields(value.scope, value.scope.kind === "task" ? ["kind", "taskId"] : ["kind"], "Schedule scope");
61
63
  jobScopeKey(value.scope);
64
+ if (value.builtin !== undefined) {
65
+ requireKnownFields(value.builtin, ["projectId", "enabled"], "Builtin Schedule");
66
+ if (!/^project-\d+$/u.test(value.builtin.projectId) || typeof value.builtin.enabled !== "boolean"
67
+ || value.scope.kind !== "global" || value.id !== `project-sync-${value.builtin.projectId}`
68
+ || value.jobId !== value.id || value.target.kind !== "existing-job"
69
+ || value.authorization.kind !== "service")
70
+ throw new Error("Invalid builtin Schedule.");
71
+ }
62
72
  validateScheduleRule(value.rule);
63
73
  requirePositiveInteger(value.revision, "Schedule revision");
64
74
  requirePositiveInteger(value.generation, "Schedule generation");
@@ -74,35 +84,75 @@ export function validateSchedule(value) {
74
84
  instant(value.processedThrough);
75
85
  instant(value.createdAt);
76
86
  instant(value.updatedAt);
77
- requireKnownFields(value.authorization, ["caller", "binding", "jobRevision"], "Schedule authorization");
78
87
  requirePositiveInteger(value.authorization.jobRevision, "Schedule Job revision");
79
88
  if (typeof value.authorization.binding !== "string" || !/^[a-f0-9]{64}$/u.test(value.authorization.binding))
80
89
  throw new Error("Invalid Schedule authority binding.");
81
- const caller = value.authorization.caller;
82
- requireKnownFields(caller, ["scope", "taskId", "role", "agentId", "adapterId", "nativeSessionId", "runId"], "Schedule caller");
83
- if (typeof caller.role !== "string" || typeof caller.nativeSessionId !== "string")
84
- throw new Error("Schedule authority requires a Role and Session.");
85
- requireText(caller.role, "Schedule caller role");
86
- requireText(caller.nativeSessionId, "Schedule caller Session");
87
- if (caller.scope === "global") {
88
- if (caller.role !== "operator")
89
- throw new Error("Global Schedule authority requires Operator.");
90
+ if (value.authorization.kind === "service") {
91
+ requireKnownFields(value.authorization, ["kind", "service", "grantId", "revoked", "binding", "jobRevision", "grantedBy", "grantedAt"], "Schedule service authorization");
92
+ if (value.scope.kind !== "global" || value.authorization.service !== "schedule"
93
+ || typeof value.authorization.revoked !== "boolean")
94
+ throw new Error("Invalid Schedule service scope or registration.");
95
+ scheduleId(value.authorization.grantId);
96
+ instant(value.authorization.grantedAt);
97
+ if (value.authorization.grantedBy.scope === "service") {
98
+ requireKnownFields(value.authorization.grantedBy, ["scope", "service"], "Builtin service issuer");
99
+ if (value.authorization.grantedBy.service !== "project-sync" || value.builtin === undefined) {
100
+ throw new Error("Invalid builtin service issuer.");
101
+ }
102
+ }
103
+ else {
104
+ const manager = parseDurableJobCaller(value.authorization.grantedBy);
105
+ if ((manager.scope === "user" && Object.keys(manager).length !== 1)
106
+ || (manager.scope !== "user" && (manager.scope !== "global" || manager.role !== "operator" || !manager.nativeSessionId))) {
107
+ throw new Error("Invalid service grant issuer.");
108
+ }
109
+ }
90
110
  }
91
- else if (caller.scope !== "task" || value.scope.kind !== "task" || caller.taskId !== value.scope.taskId) {
92
- throw new Error("Schedule caller must belong to its scope.");
111
+ else {
112
+ requireKnownFields(value.authorization, ["kind", "caller", "binding", "jobRevision"], "Schedule authorization");
113
+ if (value.authorization.kind !== "session")
114
+ throw new Error("Explicit Schedule authorization kind is required.");
115
+ const caller = value.authorization.caller;
116
+ requireKnownFields(caller, ["scope", "taskId", "role", "agentId", "adapterId", "nativeSessionId", "runId"], "Schedule caller");
117
+ if (typeof caller.role !== "string" || typeof caller.nativeSessionId !== "string")
118
+ throw new Error("Schedule authority requires a Role and Session.");
119
+ requireText(caller.role, "Schedule caller role");
120
+ requireText(caller.nativeSessionId, "Schedule caller Session");
121
+ if (caller.scope === "global") {
122
+ if (caller.role !== "operator")
123
+ throw new Error("Global Schedule authority requires Operator.");
124
+ }
125
+ else if (caller.scope !== "task" || value.scope.kind !== "task" || caller.taskId !== value.scope.taskId) {
126
+ throw new Error("Schedule caller must belong to its scope.");
127
+ }
93
128
  }
94
129
  if (value.reason !== undefined)
95
130
  requireText(value.reason, "Schedule reason");
96
131
  if (value.lastAttempt !== undefined) {
97
- requireKnownFields(value.lastAttempt, ["at", "dueAt", "outcome", "runId", "reason"], "Schedule attempt");
132
+ requireKnownFields(value.lastAttempt, ["at", "dueAt", "outcome", "runId", "jobId", "generation", "reason"], "Schedule attempt");
98
133
  instant(value.lastAttempt.at);
99
134
  instant(value.lastAttempt.dueAt);
100
135
  if (!["admitted", "overlap", "blocked"].includes(value.lastAttempt.outcome))
101
136
  throw new Error("Invalid Schedule attempt.");
102
137
  if (value.lastAttempt.runId !== undefined)
103
138
  requireText(value.lastAttempt.runId, "Schedule Run id");
139
+ if (value.lastAttempt.jobId !== undefined)
140
+ scheduleId(value.lastAttempt.jobId);
141
+ if (value.lastAttempt.generation !== undefined)
142
+ requirePositiveInteger(value.lastAttempt.generation, "Schedule attempt generation");
104
143
  if (value.lastAttempt.reason !== undefined)
105
144
  requireText(value.lastAttempt.reason, "Schedule attempt reason");
106
145
  }
107
146
  return value;
108
147
  }
148
+ export function validateScheduleTarget(target) {
149
+ if (target?.kind === "existing-job")
150
+ requireKnownFields(target, ["kind"], "Schedule target");
151
+ else if (target?.kind === "new-job") {
152
+ requireKnownFields(target, ["kind", "definitionRevision"], "Schedule target");
153
+ requirePositiveInteger(target.definitionRevision, "Schedule source definition revision");
154
+ }
155
+ else
156
+ throw new Error("Invalid Schedule target.");
157
+ return target;
158
+ }