@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
@@ -1,4 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { assertJobRoleRunnable } from "../job/agentRoleScope.js";
3
+ import { recordJobAgentObservation } from "../job/agentJobObservation.js";
2
4
  import { isDeepStrictEqual } from "node:util";
3
5
  import { nonReplayableMessageWake, pinMessageRevisions, taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
4
6
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
@@ -275,6 +277,8 @@ export class FileSchedulerStoreAdapter {
275
277
  && input.kind !== "goal.updated" && input.kind !== "goal.cleared") {
276
278
  this.persistRuntimeObservation(input, now);
277
279
  }
280
+ if (outcome === "applied")
281
+ recordJobAgentObservation(this.store, { scope: "task", taskId, roleName: input.fence.roleName }, this.store.getTaskRoleSessionSet(taskId, input.fence.roleName)?.providerBinding, now);
278
282
  return outcome;
279
283
  }
280
284
  /**
@@ -490,6 +494,7 @@ export class FileSchedulerStoreAdapter {
490
494
  });
491
495
  }
492
496
  store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, binding, now));
497
+ recordJobAgentObservation(store, { scope: "global", roleName: fence.roleName }, binding, now);
493
498
  settleGlobalRetryInput(store, fence.roleName, binding, now);
494
499
  if (["turn.completed", "turn.failed", "turn.cancelled"].includes(input.kind)) {
495
500
  checkInboxTurn(store, { scope: "global", roleName: fence.roleName, agentId: fence.agentId,
@@ -1129,6 +1134,9 @@ export class FileSchedulerStoreAdapter {
1129
1134
  beginAgentHostProviderTurn(input) {
1130
1135
  if (input.taskId === undefined) {
1131
1136
  this.store.transaction((store) => {
1137
+ const role = store.getGlobalRole(input.roleName);
1138
+ if (role)
1139
+ assertJobRoleRunnable(store, role);
1132
1140
  if (input.runId !== undefined || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: input.roleName })))) {
1133
1141
  throw new AgentHostProviderTurnFenceError("Global Session admission is stopped.");
1134
1142
  }
@@ -1326,6 +1334,7 @@ export class FileSchedulerStoreAdapter {
1326
1334
  ? { retry: { ...resolved.retry, failureRef } } : {}) };
1327
1335
  }
1328
1336
  store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, resolved, input.now));
1337
+ recordJobAgentObservation(store, { scope: "global", roleName: input.roleName }, resolved, input.now);
1329
1338
  settleGlobalRetryInput(store, input.roleName, resolved, input.now);
1330
1339
  if (input.status !== "deferred" && !providerRetryPending(resolved)) {
1331
1340
  recordGlobalRuntimeAttention(store, input.roleName, `${input.attemptId}:submission`, {
@@ -1561,7 +1570,9 @@ export class FileSchedulerStoreAdapter {
1561
1570
  if ("taskId" in next)
1562
1571
  throw new Error("A global JobRun cannot change scope.");
1563
1572
  store.saveGlobalJobRun(next);
1564
- if (wakeup !== undefined)
1573
+ // Public and explicitly service-owned work have no Agent recipient.
1574
+ // Results stay queryable without implicitly starting an Operator.
1575
+ if (wakeup !== undefined && !["user", "service:schedule"].includes(current.operation.actorId))
1565
1576
  enqueueWork(store, { kind: "operator" }, "global-job-finished", now, [{ type: "global-job-run", id: jobId }], { source: "global-job", dedupeKey: `operator-global-job-run:${jobId}` });
1566
1577
  return next;
1567
1578
  }
@@ -7,6 +7,7 @@ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
7
7
  import { globalMessagePresentation } from "../interaction/notificationPresentation.js";
8
8
  import { recordGlobalRuntimeAttention, recordGlobalRuntimeRecovery } from "./globalRuntimeAttention.js";
9
9
  import { globalInboxRecovery } from "../message/globalInboxRecovery.js";
10
+ import { assertJobRoleRunnable } from "../job/agentRoleScope.js";
10
11
  /** Messages are the only queue. Host's atomic begin arbitrates native writers. */
11
12
  export async function deliverGlobalInputs(home, store, ensure, onError, options = {}) {
12
13
  for (const role of store.listGlobalRoles()) {
@@ -17,7 +18,12 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
17
18
  let sessions = store.getGlobalRoleSessionSet(role.name);
18
19
  // A detached Host keeps an active Session and may reconnect. An explicit
19
20
  // Session stop/switch must not be undone by background notification work.
20
- if (sessions !== null && sessions.sessions[sessions.activeAgentId]?.status !== "active")
21
+ const selected = sessions?.sessions[sessions.activeAgentId];
22
+ // An explicit Job session-new archives/removes the proven-ended writer.
23
+ // Its next original input may launch normally; an ended selected Session
24
+ // still means stopped and is never implicitly restarted.
25
+ if (sessions !== null && selected?.status !== "active"
26
+ && !(role.jobOwner && selected === undefined))
21
27
  continue;
22
28
  if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
23
29
  store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
@@ -110,6 +116,7 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
110
116
  pending = pending.filter(message => !obsoleteIds.has(message.id));
111
117
  if (pending.length === 0 && !recovering)
112
118
  continue;
119
+ assertJobRoleRunnable(store, role);
113
120
  if (reservedPage.count > reserved.length && obsolete.length === reserved.length)
114
121
  continue;
115
122
  if (current != null && ["submitting", "accepted", "delivery-unknown"].includes(current.status))
@@ -0,0 +1,111 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { requireKnownFields } from "../domain/validation.js";
3
+ import { runReference } from "../job/jobBusinessResult.js";
4
+ import { createJobDefinitionControl } from "./jobDefinitionControl.js";
5
+ import { assertCallerAuthorized } from "./jobControl.js";
6
+ import { artifactInputBytes, presentArtifactContent } from "../artifacts/artifactCapability.js";
7
+ import { previewArtifactContent } from "../web/webMaterials.js";
8
+ import { boundedJobArtifactPayload, MAX_JOB_ARTIFACT_RESPONSE_BYTES } from "../artifacts/jobArtifactTransport.js";
9
+ import { jobArtifactOwner, openJobArtifactRepository, readJobArtifactReference, validateJobArtifactReference } from "../artifacts/jobArtifact.js";
10
+ /** Git commits precede database publication. A failed publication retains the exact
11
+ * commit and returns its reference for explicit registration, never business replay.
12
+ * Never hold a synchronous SQLite transaction across awaited Git operations. */
13
+ export async function controlJobArtifact(store, home, value, now) {
14
+ if (!value || typeof value !== "object" || Array.isArray(value))
15
+ throw new Error("Invalid Job artifact request.");
16
+ requireKnownFields(value, ["op", "scope", "caller", "id", "runId", "artifact", "reference", "offset"], "Job artifact request");
17
+ const input = value;
18
+ const current = () => createJobDefinitionControl(store, home)({
19
+ op: "run-get", scope: input.scope, caller: input.caller, id: input.id, runId: input.runId
20
+ }, now);
21
+ const run = current(); // Both definition and historical Assignment authorization.
22
+ const repo = openJobArtifactRepository(home, jobArtifactOwner(run), runReference(run));
23
+ if (input.op === "artifact-list") {
24
+ const head = await repo.head();
25
+ const files = head === null ? [] : await repo.list(head);
26
+ if (files.length > 256)
27
+ throw new Error("Artifact repository exceeds the bounded 256-file listing.");
28
+ const saved = [];
29
+ if (head !== null)
30
+ for (const file of files) {
31
+ // One bounded file at a time: a valid 256-file repository must not spawn
32
+ // hundreds of Git children or materialize all 8 MiB blobs concurrently.
33
+ const content = await repo.read(file.relativePath, head);
34
+ saved.push({ kind: "artifact", owner: jobArtifactOwner(run), sourceRun: runReference(run),
35
+ version: head, relativePath: file.relativePath, digest: content.digest });
36
+ }
37
+ current();
38
+ return { sourceRun: runReference(run), artifacts: run.artifacts ?? [],
39
+ repository: { head, saved, workingChanges: await repo.workingChanges() } };
40
+ }
41
+ if (input.op === "artifact-read" || input.op === "artifact-preview") {
42
+ const ref = validateJobArtifactReference(input.reference, run);
43
+ // This grants only a deliberately selected, published object, never HEAD or
44
+ // all material in its Task. An arbitrary caller-supplied Task ref is not a grant.
45
+ if (!(run.artifacts ?? []).some(item => isDeepStrictEqual(item, ref))) {
46
+ throw new Error("Artifact reference is not registered for this exact Run.");
47
+ }
48
+ const content = await readJobArtifactReference(home, ref);
49
+ current(); // A replaced Session during I/O must not receive the material.
50
+ return boundedJobArtifactPayload({ reference: ref, ...(input.op === "artifact-preview"
51
+ ? previewArtifactContent(content, input.offset) : presentArtifactContent(content)) }, "Artifact response", MAX_JOB_ARTIFACT_RESPONSE_BYTES);
52
+ }
53
+ if (!["artifact-save", "artifact-register"].includes(input.op))
54
+ throw new Error("Unknown Job artifact operation.");
55
+ let ref;
56
+ if (input.op === "artifact-save") {
57
+ const artifact = input.artifact;
58
+ requireKnownFields(artifact, ["relativePath", "content", "encoding", "message", "expectedHead"], "Job artifact save");
59
+ boundedJobArtifactPayload(artifact, "Artifact save");
60
+ if (typeof artifact.relativePath !== "string" || artifact.relativePath.length > 512)
61
+ throw new Error("Artifact path exceeds 512 characters.");
62
+ if ((run.artifacts?.length ?? 0) >= 256)
63
+ throw new Error("Run artifacts exceed the bounded reference limit.");
64
+ const bytes = artifactInputBytes(artifact);
65
+ const saved = await repo.save({ files: [{ relativePath: artifact.relativePath, bytes }],
66
+ message: artifact.message ?? `Save ${artifact.relativePath}`, expectedHead: artifact.expectedHead });
67
+ const content = await repo.read(artifact.relativePath, saved.commit);
68
+ ref = { kind: "artifact", owner: jobArtifactOwner(run), sourceRun: runReference(run),
69
+ version: saved.commit, relativePath: content.relativePath, digest: content.digest };
70
+ }
71
+ else {
72
+ ref = validateJobArtifactReference(input.reference, run);
73
+ if (ref.owner.kind === "task") {
74
+ // Only a Task Leader or Operator with current access may select Task material.
75
+ // Assignment readers cannot manufacture a cross-entity grant.
76
+ if (!(input.caller.scope === "global" && input.caller.role === "operator")
77
+ && !(input.caller.scope === "task" && input.caller.role === "leader")) {
78
+ throw new Error("Selecting Task artifacts requires its authorized Leader or Operator.");
79
+ }
80
+ assertCallerAuthorized(store, input.caller, ref.owner.taskId);
81
+ }
82
+ await readJobArtifactReference(home, ref);
83
+ }
84
+ try {
85
+ return store.transaction(tx => {
86
+ const fresh = current();
87
+ if (ref.owner.kind === "task")
88
+ assertCallerAuthorized(tx, input.caller, ref.owner.taskId);
89
+ const previous = fresh.artifacts ?? [];
90
+ if (previous.some(item => isDeepStrictEqual(item, ref)))
91
+ return { status: "registered", reference: ref };
92
+ const next = { ...fresh, artifacts: [...previous, ref],
93
+ updatedAt: new Date(Math.max(now.getTime(), Date.parse(fresh.updatedAt))).toISOString() };
94
+ if ("executor" in next)
95
+ tx.saveAgentJobRun(next);
96
+ else if ("taskId" in next)
97
+ tx.saveDurableJob(next.taskId, next);
98
+ else
99
+ tx.saveGlobalJobRun(next);
100
+ return { status: "registered", reference: ref };
101
+ });
102
+ }
103
+ catch (cause) {
104
+ // An explicit partial receipt survives the RPC's bounded exception text.
105
+ // It is NOT a published result. Never replay the business action or silently
106
+ // retry the database write to conceal this boundary.
107
+ return { status: "saved-unregistered", reference: ref,
108
+ error: (cause instanceof Error ? cause.message : String(cause)).slice(0, 4096),
109
+ recovery: "Inspect artifact-list and register this exact reference; do not rerun business work." };
110
+ }
111
+ }
@@ -86,6 +86,8 @@ export function createDurableJobControl(store) {
86
86
  validateStartParams(tx, params);
87
87
  const previous = params.retryOf === undefined ? null : tx.getDurableJob(params.taskId, params.retryOf);
88
88
  if (previous !== null) {
89
+ if (previous.delegation)
90
+ throw jobDomainError("Use Job run retry for a Task delegation, not a process specification.");
89
91
  requireJobAssignment(tx, previous, params.caller);
90
92
  if (definition === null && durableJobIdempotencyKey(previous) !== specificationKey) {
91
93
  throw jobDomainError("Legacy retry must preserve the original Run specification; update its Job definition for a changed configuration.");
@@ -190,6 +192,8 @@ export function createJobCallAuthority(store) {
190
192
  * Job even when its management binding is revoked. Only a new spawn checks it.
191
193
  */
192
194
  export function authorizeJobStart(store, job) {
195
+ if (job.delegation)
196
+ throw jobDomainError("Task delegation cannot start a process.");
193
197
  const taskPrefix = `task:${job.taskId}/role:`;
194
198
  const actor = job.operation.actorId;
195
199
  const scope = actor.startsWith(taskPrefix) ? "task" : actor.startsWith("global:") ? "global" : undefined;
@@ -8,9 +8,26 @@ import { prepareBuiltinJob, projectRefreshDigest } from "../job/builtinJob.js";
8
8
  import { projectRefreshTargetDigest, requireRefreshProject } from "../repository/projectRefresh.js";
9
9
  import { assertCallerAuthorized, assertNonSecretJobInput, createDurableJobControl, jobAuthorityBinding, validateStartParams, requireJobAssignment, parseDurableJobCaller } from "./jobControl.js";
10
10
  import { createFileArtifactPort } from "./jobSupervisor.js";
11
- export function authorizeScope(store, scope, caller) {
11
+ import { jobRunSnapshot, runReference } from "../job/jobBusinessResult.js";
12
+ import { controlJobArtifact } from "./jobArtifactControl.js";
13
+ import { ArtifactHeadConflictError, ArtifactManagedConfigViolationError, ArtifactRemoteViolationError } from "../artifacts/taskArtifactRepository.js";
14
+ import { ArtifactCommitLockedError } from "../artifacts/artifactCommitLock.js";
15
+ import { ManagedGitError } from "../artifacts/managedGit.js";
16
+ import { authorizeServiceJob, requireScheduleService, serviceRunBinding } from "../job/scheduleService.js";
17
+ import { authorizeAgentDefinition, controlAgentJob, summarizeAgentRuns } from "./agentJobControl.js";
18
+ import { admitTaskJob, authorizeTaskJob, reconcileTaskDelegation, taskDelegationView } from "./jobTaskDelegationControl.js";
19
+ import { requireProjectSyncTarget } from "../job/projectSync.js";
20
+ export function authorizeScope(store, scope, caller, allowPublic = false) {
12
21
  parseDurableJobCaller(caller);
13
22
  jobScopeKey(scope);
23
+ // Public local ingress is not a fabricated Operator. It grants only immediate
24
+ // controlled global management/actions. Schedule grants are explicit and
25
+ // Task-shell authority is unchanged.
26
+ if (allowPublic && scope.kind === "global" && caller.scope === "user") {
27
+ if (Object.keys(caller).some(key => key !== "scope"))
28
+ throw new Error("Public user Job caller must not claim a managed identity.");
29
+ return;
30
+ }
14
31
  // Match Task Context's Operator-only boundary for global callers as well
15
32
  // as excluding Task callers from global Jobs. Session identity alone is not
16
33
  // permission for a custom GlobalRole to read or manage Task definitions.
@@ -26,7 +43,13 @@ function taskParams(job, caller, requestId) {
26
43
  return { ...job.configuration, taskId: job.scope.taskId, caller, requestId };
27
44
  }
28
45
  export function authorizeDefinition(store, job, caller, write) {
29
- authorizeScope(store, job.scope, caller);
46
+ if (job.configuration.kind === "agent")
47
+ return authorizeAgentDefinition(store, job, caller, write);
48
+ authorizeScope(store, job.scope, caller, true);
49
+ if (job.configuration.kind === "task") {
50
+ authorizeTaskJob(store, job, caller, write);
51
+ return;
52
+ }
30
53
  if (job.scope.kind === "task") {
31
54
  const params = taskParams(job, caller, "definition-validation");
32
55
  // A definition must not reveal another Assignment's commands/environment.
@@ -41,11 +64,13 @@ export function authorizeDefinition(store, job, caller, write) {
41
64
  throw new Error("Global Jobs require builtin actions.");
42
65
  prepareBuiltinJob(job.configuration, "/");
43
66
  if (job.configuration.action === "project.refresh") {
67
+ if (job.builtin)
68
+ requireProjectSyncTarget(store, job);
44
69
  const project = requireRefreshProject(store, job.configuration.projectId, true);
45
70
  if (projectRefreshDigest(job.configuration) !== projectRefreshTargetDigest(project)) {
46
71
  throw new Error("Project refresh target changed; inspect the Project and explicitly update the Job.");
47
72
  }
48
- const duplicate = store.listJobDefinitions({ kind: "global" }).find(other => other.id !== job.id && other.configuration.kind === "builtin"
73
+ const duplicate = !job.instantiation && store.listJobDefinitions({ kind: "global" }).find(other => !other.instantiation && !other.builtin && other.id !== job.id && other.configuration.kind === "builtin"
49
74
  && other.configuration.action === "project.refresh" && other.configuration.projectId === project.id);
50
75
  if (duplicate)
51
76
  throw new Error(`Project refresh Job already exists: ${duplicate.id}.`);
@@ -57,7 +82,29 @@ export function listJobRuns(store, job) {
57
82
  .filter(run => run.definitionId === job.id);
58
83
  }
59
84
  /** Read-only presentation; attempts/results remain owned by exact JobRuns. */
60
- function presentDefinition(store, job) {
85
+ function presentDefinition(store, definition, caller) {
86
+ if (definition.configuration.kind === "agent")
87
+ return {
88
+ ...definition, results: summarizeAgentRuns(store.listAgentJobRuns(definition.scope, definition.id))
89
+ };
90
+ const history = listJobRuns(store, definition).filter(run => {
91
+ try {
92
+ authorizeRunRead(store, run, caller);
93
+ return true;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }).sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
99
+ const result = (run) => run ? {
100
+ runId: run.id, sourceRun: runReference(run), status: run.status,
101
+ outcome: run.businessResult?.outcome ?? null,
102
+ summary: run.businessResult?.summary.slice(0, 512) ?? null,
103
+ artifactCount: run.artifacts?.length ?? 0, delegation: taskDelegationView(store, run)
104
+ } : null;
105
+ const job = { ...definition, results: {
106
+ latest: result(history[0]), latestSuccessful: result(history.find(run => run.businessResult?.outcome === "succeeded"))
107
+ } };
61
108
  if (job.configuration.kind !== "builtin" || job.configuration.action !== "project.refresh")
62
109
  return job;
63
110
  const projectId = job.configuration.projectId;
@@ -69,14 +116,21 @@ function presentDefinition(store, job) {
69
116
  .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
70
117
  const summarize = (run) => run === undefined ? null : {
71
118
  runId: run.id, definitionRevision: run.definitionRevision, trigger: run.trigger,
72
- status: run.status, attemptedAt: run.createdAt, finishedAt: run.terminalAt ?? null
119
+ status: run.status, attemptedAt: run.createdAt, finishedAt: run.terminalAt ?? null,
120
+ summary: run.businessResult?.summary.slice(0, 512) ?? null
73
121
  };
74
122
  return { ...job, refresh: {
75
123
  projectId, lastAttempt: summarize(runs[0]),
76
124
  lastSuccessfulRefresh: summarize(runs.find(run => run.status === "succeeded"))
77
125
  } };
78
126
  }
79
- function authorizeRunRead(store, run, caller) {
127
+ export function authorizeRunRead(store, run, caller) {
128
+ if (run.delegation) {
129
+ authorizeScope(store, runReference(run).scope, caller, true);
130
+ if (caller.scope === "task" && caller.role !== "leader")
131
+ throw new Error("Task delegation is not an Assignment grant.");
132
+ return;
133
+ }
80
134
  // A revised definition's Assignment does not grant access to earlier Runs.
81
135
  if ("taskId" in run)
82
136
  createDurableJobControl(store).getJob(run.taskId, run.id, caller);
@@ -86,12 +140,21 @@ function authorizeRunRead(store, run, caller) {
86
140
  * schedule advancement. No external effects occur here; rollback removes the
87
141
  * admission as well. The caller must be freshly authenticated on every call.
88
142
  */
89
- export function admitJobRun(store, home, scope, definitionId, caller, trigger, now, retryOf) {
143
+ export function admitJobRun(store, home, scope, definitionId, caller, trigger, now, retryOf, service) {
90
144
  return store.transaction(tx => {
91
145
  const job = tx.getJobDefinition(scope, definitionId);
92
146
  if (job === null)
93
147
  throw new Error("Job definition not found.");
148
+ if (job.configuration.kind === "agent")
149
+ throw new Error("Agent Jobs use their explicit Role message admission; Schedule service does not grant Agent execution.");
94
150
  authorizeDefinition(tx, job, caller, false);
151
+ const serviceSchedule = service === undefined ? undefined : authorizeServiceJob(tx, job, service, trigger.key);
152
+ if (service && (scope.kind !== "global" || trigger.source !== "schedule" || retryOf !== undefined)) {
153
+ throw new Error("Invalid Schedule service admission.");
154
+ }
155
+ if (!service && caller.scope === "user" && !["manual", "retry"].includes(trigger.source)) {
156
+ throw new Error("Public Job admission only authorizes an immediate manual execution or explicit retry.");
157
+ }
95
158
  if (!trigger.key.trim() || !["manual", "retry", "schedule", "integration"].includes(trigger.source)) {
96
159
  throw new Error("Invalid JobRun trigger.");
97
160
  }
@@ -116,6 +179,11 @@ export function admitJobRun(store, home, scope, definitionId, caller, trigger, n
116
179
  throw new Error("Retry requires an exact failed/cancelled/timed-out Run of this Job.");
117
180
  }
118
181
  const requestId = `job:${job.id}:${trigger.source}:${trigger.key}`;
182
+ if (job.configuration.kind === "task") {
183
+ if (service)
184
+ throw new Error("Schedule service does not authorize Task delegation.");
185
+ return { run: admitTaskJob(tx, { ...job, configuration: job.configuration }, caller, trigger, requestId, now, retryOf), created: true };
186
+ }
119
187
  if (scope.kind === "task") {
120
188
  const result = createDurableJobControl(tx).startJob({
121
189
  ...taskParams(job, caller, requestId), definitionId, trigger, retryOf
@@ -127,30 +195,85 @@ export function admitJobRun(store, home, scope, definitionId, caller, trigger, n
127
195
  const id = `job-${randomUUID()}`;
128
196
  const locator = `artifacts/jobs/global/${id}`;
129
197
  const spec = prepareBuiltinJob(job.configuration, join(home, locator), { home, runId: id });
130
- const actorId = `global:${caller.role}`;
131
- const run = validateDurableJob({
198
+ const actorId = service ? "service:schedule" : caller.scope === "user" ? "user" : `global:${caller.role}`;
199
+ let run = {
132
200
  schemaVersion: 1, scope: "global", id, definitionId: job.id, definitionRevision: job.revision,
133
201
  trigger: structuredClone(trigger), action: job.configuration.action,
202
+ ...(service === undefined ? {} : { serviceAuthorization: structuredClone(service) }),
134
203
  actionInput: structuredClone(job.configuration.input),
135
204
  ...(job.configuration.projectId === undefined ? {} : { projectId: job.configuration.projectId }),
136
205
  ...spec, status: "queued", artifactsLocator: locator,
137
206
  idempotencyKey: createHash("sha256").update(JSON.stringify([actorId, requestId])).digest("hex"),
138
207
  operation: {
139
- requestId, actorId, authorityRef: jobAuthorityBinding(tx, "global", caller.role, ""),
208
+ requestId, actorId, authorityRef: service || caller.scope === "user" ? "pending" : jobAuthorityBinding(tx, "global", caller.role, ""),
140
209
  targetId: job.id, capability: "job.run", implementation: JOB_RUNNER_IMPLEMENTATION,
141
210
  inputDigest: createHash("sha256").update(JSON.stringify([job, trigger, retryOf])).digest("hex"),
142
211
  effect: "none", receiptRefs: [], partialResultRefs: []
143
212
  },
144
213
  ...(retryOf === undefined ? {} : { retryOf }),
145
214
  createdAt: now.toISOString(), updatedAt: now.toISOString()
146
- });
215
+ };
216
+ if (serviceSchedule)
217
+ run = { ...run, operation: { ...run.operation, authorityRef: serviceRunBinding(tx, run, serviceSchedule) } };
218
+ else if (caller.scope === "user")
219
+ run = { ...run, operation: { ...run.operation, authorityRef: publicJobBinding(tx, run) } };
220
+ validateDurableJob(run);
147
221
  tx.saveGlobalJobRun(run);
148
222
  return { run, created: true };
149
223
  });
150
224
  }
225
+ /** Job owns instantiation; Schedule supplies only the exact version and occurrence. */
226
+ export function instantiateJobRun(store, home, scope, definitionId, revision, caller, trigger, now, service) {
227
+ return store.transaction(tx => {
228
+ const source = tx.getJobDefinition(scope, definitionId);
229
+ if (!source || source.revision !== revision)
230
+ throw new Error("Source Job revision changed; explicitly select an available version.");
231
+ authorizeDefinition(tx, source, caller, true);
232
+ const id = `instance-${createHash("sha256").update(JSON.stringify([source.scope, source.id, trigger])).digest("hex")}`;
233
+ const instantiation = { sourceId: source.id, sourceRevision: revision, key: trigger.key };
234
+ let job = tx.getJobDefinition(source.scope, id);
235
+ if (job) {
236
+ if (JSON.stringify(job.instantiation) !== JSON.stringify(instantiation)
237
+ || JSON.stringify(job.configuration) !== JSON.stringify(source.configuration)) {
238
+ throw new Error("Job instance identity conflicts with its original source.");
239
+ }
240
+ }
241
+ else {
242
+ job = validateJobDefinition({ ...createJobDefinition({ id, scope: source.scope,
243
+ title: source.title, configuration: source.configuration }, now), instantiation });
244
+ tx.saveJobDefinition(job);
245
+ }
246
+ return admitJobRun(tx, home, source.scope, job.id, caller, trigger, now, undefined, service);
247
+ });
248
+ }
249
+ function publicJobBinding(store, run) {
250
+ return createHash("sha256").update(JSON.stringify([
251
+ "public-immediate-job/v1", store.getHomeIdentity().homeId, run.id, run.definitionId, run.definitionRevision,
252
+ run.action, run.projectId, run.actionInput, run.trigger, run.operation.requestId
253
+ ])).digest("hex");
254
+ }
151
255
  export function authorizeGlobalJobStart(store, run) {
152
- if (run.operation.actorId !== "global:operator"
153
- || jobAuthorityBinding(store, "global", "operator", "") !== run.operation.authorityRef) {
256
+ if (run.delegation)
257
+ throw new Error("Task delegation cannot start a builtin process.");
258
+ let serviceAdmission = false;
259
+ if (run.operation.actorId === "service:schedule" && run.serviceAuthorization && run.trigger.source === "schedule") {
260
+ try {
261
+ const schedule = requireScheduleService(store, run.serviceAuthorization);
262
+ const job = store.getJobDefinition({ kind: "global" }, run.definitionId);
263
+ if (!job || job.revision !== run.definitionRevision)
264
+ throw new Error("Schedule Run Job revision changed.");
265
+ authorizeServiceJob(store, job, run.serviceAuthorization, run.trigger.key);
266
+ serviceAdmission = serviceRunBinding(store, run, schedule) === run.operation.authorityRef;
267
+ }
268
+ catch (error) {
269
+ throw Object.assign(new Error(error instanceof Error ? error.message : String(error), { cause: error }), { name: "CoreJobError" });
270
+ }
271
+ }
272
+ const publicAdmission = run.operation.actorId === "user"
273
+ && ["manual", "retry"].includes(run.trigger.source)
274
+ && publicJobBinding(store, run) === run.operation.authorityRef;
275
+ if (!serviceAdmission && !publicAdmission && (run.operation.actorId !== "global:operator"
276
+ || jobAuthorityBinding(store, "global", "operator", "") !== run.operation.authorityRef)) {
154
277
  throw Object.assign(new Error("Global JobRun caller binding was revoked; no execution was started."), { name: "CoreJobError" });
155
278
  }
156
279
  // Storage failures are not domain refusals or proof of no execution.
@@ -162,6 +285,9 @@ export function authorizeGlobalJobStart(store, run) {
162
285
  ...(run.projectId === undefined ? {} : { projectId: run.projectId })
163
286
  }, run.workspace, { home, runId: run.id });
164
287
  if (run.action === "project.refresh") {
288
+ const definition = store.getJobDefinition({ kind: "global" }, run.definitionId);
289
+ if (definition?.builtin)
290
+ requireProjectSyncTarget(store, definition);
165
291
  const project = requireRefreshProject({ listProjects: () => projects, rootDirectory: () => home }, run.projectId, true);
166
292
  if (projectRefreshTargetDigest(project) !== projectRefreshDigest({
167
293
  kind: "builtin", action: run.action, projectId: run.projectId, input: run.actionInput
@@ -185,11 +311,28 @@ export function createJobDefinitionControl(store, home) {
185
311
  return (value, now) => {
186
312
  if (value === null || typeof value !== "object" || Array.isArray(value))
187
313
  throw new Error("Invalid Job request.");
188
- requireKnownFields(value, ["op", "scope", "caller", "id", "title", "configuration", "expectedRevision", "trigger", "retryOf", "runId"], "Job request");
314
+ const op = value.op;
315
+ if (typeof op === "string" && op.startsWith("artifact-")) {
316
+ return controlJobArtifact(store, home, value, now).catch(error => {
317
+ // Preserve bounded domain diagnostics through RPC; authentication errors
318
+ // retain their existing CoreApplicationError identity and code.
319
+ if (error instanceof ArtifactHeadConflictError || error instanceof ArtifactManagedConfigViolationError
320
+ || error instanceof ArtifactRemoteViolationError || error instanceof ArtifactCommitLockedError
321
+ || error instanceof ManagedGitError)
322
+ error.name = "CoreJobError";
323
+ throw error;
324
+ });
325
+ }
326
+ requireKnownFields(value, ["op", "scope", "caller", "id", "title", "configuration", "expectedRevision", "trigger", "retryOf", "runId", "result", "expectedSessionId", "expectedAttemptId"], "Job request");
189
327
  const input = value;
190
- authorizeScope(store, input.scope, input.caller);
328
+ // A Job execution reader is authorized against its exact definition below,
329
+ // not promoted to Operator or to a Task manager.
330
+ if (!(input.caller.scope === "global" && input.caller.role !== "operator")
331
+ && !(input.caller.scope === "task" && input.scope.kind === "global"))
332
+ authorizeScope(store, input.scope, input.caller, true);
191
333
  return store.transaction(tx => {
192
334
  if (input.op === "list") {
335
+ authorizeScope(tx, input.scope, input.caller, true);
193
336
  return tx.listJobDefinitions(input.scope).filter(job => {
194
337
  try {
195
338
  authorizeDefinition(tx, job, input.caller, false);
@@ -198,12 +341,15 @@ export function createJobDefinitionControl(store, home) {
198
341
  catch {
199
342
  return false;
200
343
  }
201
- }).map(job => presentDefinition(tx, job));
344
+ }).map(job => presentDefinition(tx, job, input.caller));
202
345
  }
203
346
  if (typeof input.id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u.test(input.id))
204
347
  throw new Error("An explicit safe Job id is required.");
205
348
  const existing = tx.getJobDefinition(input.scope, input.id);
206
349
  if (input.op === "define" || input.op === "update") {
350
+ if (existing?.builtin || (!existing && input.scope.kind === "global" && input.id.startsWith("project-sync-"))) {
351
+ throw new Error("Builtin Project sync definitions are installer-owned; edit their Schedule.");
352
+ }
207
353
  let configuration = input.configuration;
208
354
  // Empty input is an explicit request to bind the current catalog target.
209
355
  // A saved digest is never silently refreshed on run/retry.
@@ -238,9 +384,11 @@ export function createJobDefinitionControl(store, home) {
238
384
  if (existing === null)
239
385
  throw new Error("Job definition not found.");
240
386
  validateJobDefinition(existing);
387
+ if (existing.configuration.kind === "agent")
388
+ return controlAgentJob(tx, home, existing, input, now);
241
389
  authorizeDefinition(tx, existing, input.caller, false);
242
390
  if (input.op === "show")
243
- return presentDefinition(tx, existing);
391
+ return presentDefinition(tx, existing, input.caller);
244
392
  if (input.op === "runs")
245
393
  return listJobRuns(tx, existing).filter(run => {
246
394
  try {
@@ -263,8 +411,24 @@ export function createJobDefinitionControl(store, home) {
263
411
  if (run === undefined)
264
412
  throw new Error("Exact JobRun not found.");
265
413
  authorizeRunRead(tx, run, input.caller);
266
- if (input.op === "run-get")
267
- return run;
414
+ if (input.op === "run-delegation")
415
+ return taskDelegationView(tx, run);
416
+ if (input.op === "run-get" || input.op === "run-confirm")
417
+ return input.op === "run-confirm" && run.delegation
418
+ ? reconcileTaskDelegation(tx, run, now) : run;
419
+ if (input.op === "run-result")
420
+ return {
421
+ snapshot: jobRunSnapshot(run),
422
+ acceptedAt: run.createdAt, startedAt: run.startedAt ?? null,
423
+ confirmedAt: run.businessResult === undefined ? null : run.terminalAt ?? null,
424
+ status: run.status, result: run.businessResult ?? null, artifacts: run.artifacts ?? []
425
+ };
426
+ if (input.op === "run-evidence")
427
+ return {
428
+ sourceRun: jobRunSnapshot(run), operation: run.operation,
429
+ process: run.process ?? null, checkpoint: run.checkpoint ?? null,
430
+ processResult: run.result ?? null, artifactsLocator: run.artifactsLocator
431
+ };
268
432
  if (input.op === "logs") {
269
433
  const steps = run.result?.steps ?? createFileArtifactPort(home)
270
434
  .readCheckpoint(executionNamespace(run), run.id)?.completedSteps ?? run.checkpoint?.completedSteps ?? [];
@@ -288,6 +452,14 @@ export function createJobDefinitionControl(store, home) {
288
452
  });
289
453
  }
290
454
  if (input.op === "run-cancel" || input.op === "run-acknowledge") {
455
+ if (run.delegation) {
456
+ const next = input.op === "run-cancel" ? requestDurableJobCancel(run, now) : acknowledgeUnknownDurableJob(run, now);
457
+ if ("taskId" in next)
458
+ tx.saveDurableJob(next.taskId, next);
459
+ else
460
+ tx.saveGlobalJobRun(next);
461
+ return next;
462
+ }
291
463
  if ("taskId" in run) {
292
464
  const control = createDurableJobControl(tx);
293
465
  return input.op === "run-cancel"