@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.
- package/dist/artifacts/artifactCapability.js +14 -8
- package/dist/artifacts/artifactCommitLock.js +7 -5
- package/dist/artifacts/jobArtifact.js +93 -0
- package/dist/artifacts/jobArtifactTransport.js +13 -0
- package/dist/artifacts/taskArtifactRepository.js +58 -10
- package/dist/cli/callerDiscovery.js +18 -0
- package/dist/cli/commandCatalog.js +45 -14
- package/dist/cli/commandDiscovery.js +2 -3
- package/dist/commands/durableJobCommands.js +31 -6
- package/dist/commands/globalRoleCommands.js +5 -0
- package/dist/commands/scheduleCommands.js +14 -3
- package/dist/commands/taskCommands.js +5 -2
- package/dist/context/taskCatalog.js +6 -1
- package/dist/controlPlaneCli.js +12 -12
- package/dist/controller/agentJobControl.js +391 -0
- package/dist/controller/controller.js +24 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
- package/dist/controller/globalInputDelivery.js +8 -1
- package/dist/controller/jobArtifactControl.js +111 -0
- package/dist/controller/jobControl.js +4 -0
- package/dist/controller/jobDefinitionControl.js +191 -19
- package/dist/controller/jobSupervisor.js +83 -3
- package/dist/controller/jobTaskDelegationControl.js +193 -0
- package/dist/controller/runtime.js +7 -1
- package/dist/controller/scheduleControl.js +83 -20
- package/dist/integration/gitIntegrationService.js +2 -0
- package/dist/job/agentJob.js +131 -0
- package/dist/job/agentJobObservation.js +61 -0
- package/dist/job/agentRoleScope.js +33 -0
- package/dist/job/durableJob.js +63 -28
- package/dist/job/jobAssignmentScope.js +1 -0
- package/dist/job/jobBusinessResult.js +72 -0
- package/dist/job/jobCleanupInspection.js +4 -0
- package/dist/job/jobDefinition.js +26 -2
- package/dist/job/jobRunner.js +4 -3
- package/dist/job/projectRefreshRunner.js +54 -18
- package/dist/job/projectSync.js +162 -0
- package/dist/job/scheduleService.js +55 -0
- package/dist/job/taskDelegation.js +48 -0
- package/dist/message/messageContinuation.js +1 -1
- package/dist/resources/projectResourceService.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/role/role.js +22 -12
- package/dist/runtime/managedIdentity.js +5 -3
- package/dist/runtime/sessionCaller.js +9 -10
- package/dist/schedule/schedule.js +65 -15
- package/dist/scheduler/wakeReason.js +4 -0
- package/dist/storage/agentJobSchema.js +10 -0
- package/dist/storage/maintenanceCaller.js +17 -25
- package/dist/storage/nativeSessionLookup.js +23 -0
- package/dist/storage/recordValidation.js +2 -0
- package/dist/storage/sessionHistorySchema.js +17 -0
- package/dist/storage/sqliteSchema.js +96 -11
- package/dist/storage/sqliteStore.js +71 -4
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +4 -0
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
- package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
- package/dist/task/archivePreflight.js +15 -0
- package/dist/task/completionReadiness.js +7 -0
- package/dist/task/task.js +17 -1
- package/dist/verification/verificationGateService.js +4 -0
- package/dist/web/assets/client/app/catalog.js +16 -0
- package/dist/web/assets/client/i18n/en.js +29 -2
- package/dist/web/assets/client/i18n/zh-CN.js +29 -2
- package/dist/web/assets/client/layout/manageShell.js +9 -0
- package/dist/web/assets/client/layout/router.js +1 -0
- package/dist/web/assets/client/views/jobs.js +166 -37
- package/dist/web/assets/client/views/task/files.js +5 -4
- package/dist/web/assets/client/views/task/page.js +5 -1
- package/dist/web/taskPreviews.js +2 -2
- package/dist/web/webJobs.js +36 -11
- package/dist/web/webMaterials.js +21 -9
- package/dist/web/webServer.js +6 -5
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/authorization.md +130 -0
- package/docs/job-runs.md +392 -17
- package/docs/managed-turn-and-session-runtime.md +10 -5
- package/docs/managed-turn-and-session-runtime.zh-CN.md +12 -5
- package/docs/release-workflow.md +79 -0
- package/docs/release-workflow.zh-CN.md +58 -0
- package/docs/schedules.md +69 -15
- package/docs/storage-baseline.md +45 -1
- package/docs/storage-baseline.zh-CN.md +33 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-runtime/SKILL.md +12 -5
- 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"
|
|
17
|
-
|| run.operation.effect
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 }
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
172
|
-
const
|
|
171
|
+
: { ...globalSessions.history, [roleAgentSessionRef(targetSession)]: targetSession };
|
|
172
|
+
const taskHistory = taskSessions === null
|
|
173
173
|
? undefined
|
|
174
|
-
: (taskSessions.history ?? [])
|
|
174
|
+
: [...(taskSessions.history ?? []), targetSession];
|
|
175
175
|
updatedSessions = validateRoleSessionSet({
|
|
176
176
|
...sessions,
|
|
177
177
|
sessions: remainingSessions,
|
|
178
|
-
...(
|
|
179
|
-
...(
|
|
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
|
-
/**
|
|
2
|
-
*
|
|
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", "
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
: "
|
|
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
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
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
|
|
92
|
-
|
|
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
|
+
}
|