spawnfile 0.1.18 → 0.1.20

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 (42) hide show
  1. package/dist/compiler/containerArtifactsPlans.js +1 -0
  2. package/dist/compiler/containerArtifactsTypes.d.ts +8 -0
  3. package/dist/compiler/containerDaimonGrokWorkerProvisioning.js +21 -1
  4. package/dist/compiler/containerDaimonGrokWorkerRender.d.ts +9 -0
  5. package/dist/compiler/containerDaimonGrokWorkerRender.js +7 -2
  6. package/dist/compiler/training/broker/declaration.d.ts +7 -0
  7. package/dist/compiler/training/broker/declaration.js +13 -1
  8. package/dist/compiler/training/broker/entrypoint.js +7 -10
  9. package/dist/compiler/training/broker/index.d.ts +2 -1
  10. package/dist/compiler/training/broker/index.js +2 -1
  11. package/dist/compiler/training/broker/projection.d.ts +9 -0
  12. package/dist/compiler/training/broker/projection.js +20 -1
  13. package/dist/compiler/training/broker/recycleClient.d.ts +33 -0
  14. package/dist/compiler/training/broker/recycleClient.js +112 -0
  15. package/dist/compiler/training/broker/registration.d.ts +3 -0
  16. package/dist/compiler/training/broker/registration.js +3 -0
  17. package/dist/compiler/training/broker/rootScript.d.ts +32 -0
  18. package/dist/compiler/training/broker/rootScript.js +33 -0
  19. package/dist/compiler/training/broker/runtime.js +4 -3
  20. package/dist/compiler/training/index.d.ts +3 -0
  21. package/dist/compiler/training/index.js +3 -0
  22. package/dist/compiler/training/preparation/assets/package-lock.json +2 -2
  23. package/dist/compiler/training/preparation/broker.d.ts +10 -1
  24. package/dist/compiler/training/preparation/broker.js +25 -3
  25. package/dist/compiler/training/preparation/prepare.js +5 -4
  26. package/dist/compiler/training/subjectDeclaration.d.ts +25 -0
  27. package/dist/compiler/training/subjectDeclaration.js +52 -0
  28. package/dist/compiler/training/subjectLease.d.ts +114 -0
  29. package/dist/compiler/training/subjectLease.js +175 -0
  30. package/dist/runtime/daimon/adapter.d.ts +7 -9
  31. package/dist/runtime/daimon/adapter.js +14 -19
  32. package/dist/runtime/daimon/config.js +13 -1
  33. package/dist/runtime/daimon/grokSkillsSupport.d.ts +20 -0
  34. package/dist/runtime/daimon/grokSkillsSupport.js +61 -0
  35. package/dist/runtime/daimon/grokWorkerConfigBytes.d.ts +126 -0
  36. package/dist/runtime/daimon/grokWorkerConfigBytes.js +157 -0
  37. package/dist/runtime/daimon/grokWorkerSkills.d.ts +61 -0
  38. package/dist/runtime/daimon/grokWorkerSkills.js +107 -0
  39. package/dist/runtime/types.d.ts +8 -0
  40. package/moltnet-releases.json +4 -4
  41. package/package.json +1 -1
  42. package/runtimes.yaml +5 -5
@@ -1,5 +1,8 @@
1
- import { lstat, writeFile } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
4
+ import { daimonAgentSkillsFromResolved, parseDaimonAgentSkills } from "../../../runtime/daimon/grokWorkerSkills.js";
5
+ import { SpawnfileError } from "../../../shared/index.js";
3
6
  import { DAIMON_GROK_SECCOMP_PROFILE_SHA256 } from "../../../shared/daimonGrokSeccompProfile.js";
4
7
  import { assertNotDesktopGrokAuth } from "../broker/entrypoint.js";
5
8
  import { parseTrainingBrokerDeclaration } from "../broker/declaration.js";
@@ -15,7 +18,25 @@ import { DAIMON_ORGANIZATION_UID } from "../../../runtime/daimon/runtimeIdentity
15
18
  * `docker create` — a dry run, a failed build — must still refuse the
16
19
  * developer's desktop Grok login rather than report it later.
17
20
  */
18
- export const prepareTrainingBroker = async (broker, root, staging, write, slots = 4) => {
21
+ /**
22
+ * The subject's declared skills, read from the context's pinned sources and
23
+ * refused if any byte changed since capture: exactly the `SKILL.md` files the
24
+ * compiler emits into its `.agents/skills`, in Daimon's engine-neutral form.
25
+ * `undefined` for none keeps a skill-less declaration byte-identical.
26
+ */
27
+ export const trainingBrokerSkills = async (context) => {
28
+ if (context.skills.length === 0)
29
+ return undefined;
30
+ const skills = await Promise.all(context.skills.map(async (skill) => {
31
+ const bytes = await readFile(skill.sourcePath);
32
+ if (`sha256:${createHash("sha256").update(bytes).digest("hex")}` !== skill.sha256) {
33
+ throw new SpawnfileError("validation_error", `Training skill ${skill.name} changed since its context was captured`);
34
+ }
35
+ return { name: skill.name, content: bytes.toString("utf8") };
36
+ }));
37
+ return parseDaimonAgentSkills(daimonAgentSkillsFromResolved(skills));
38
+ };
39
+ export const prepareTrainingBroker = async (broker, root, staging, write, slots = 4, skills) => {
19
40
  const bootstrap = path.resolve(root, broker.bootstrap);
20
41
  assertNotDesktopGrokAuth(bootstrap);
21
42
  const info = await lstat(bootstrap).catch(() => undefined);
@@ -36,7 +57,8 @@ export const prepareTrainingBroker = async (broker, root, staging, write, slots
36
57
  bootstrap: TRAINING_BOOTSTRAP_MOUNT,
37
58
  organizationUid: DAIMON_ORGANIZATION_UID,
38
59
  seccompProfileSha256: DAIMON_GROK_SECCOMP_PROFILE_SHA256,
39
- ...(broker.unenforcedBindPolicy === undefined ? {} : { unenforcedBindPolicy: broker.unenforcedBindPolicy })
60
+ ...(broker.unenforcedBindPolicy === undefined ? {} : { unenforcedBindPolicy: broker.unenforcedBindPolicy }),
61
+ ...(skills?.length ? { skills } : {})
40
62
  });
41
63
  const declarationPath = path.join(staging, "training-broker.json");
42
64
  if (write)
@@ -8,7 +8,7 @@ import { assertInputRoot, exactPath, fileIdentity, hashJson, sealTree, within }
8
8
  import { planInputs, readBoundedJson, stageInput, verifyCanonicalPins } from "./inputs.js";
9
9
  import { planTrainingImage, buildTrainingImage } from "./image.js";
10
10
  import { planMeasurementRepair, stageMeasurementRepair, validateMeasurementRepairOutput, writeTrainingWitness } from "../repair/index.js";
11
- import { prepareTrainingBroker } from "./broker.js";
11
+ import { prepareTrainingBroker, trainingBrokerSkills } from "./broker.js";
12
12
  import { claimTrainingPreparationScratch, removeTrainingPreparationScratch } from "./scratch.js";
13
13
  function mappedReceipt(digest, image, config) {
14
14
  return parseTrainingMappedPreparation({ version: "spawnfile.training-preparation.v1", preparationDigest: digest, imageId: image,
@@ -88,8 +88,9 @@ export async function prepareTraining(options) {
88
88
  const digest = hashJson({ config, sources: inputs.map(input => ({ id: input.id, digest: input.digest })), image: imagePlan?.digest ?? config.image,
89
89
  canonical: options.context.project.sourceDigest,
90
90
  ...(options.execution ? { execution: trainingExecutionIdentity(options.execution) } : {}), ...(repair ? { repair: { witness: repair.witness.digest, parent: repair.manifestDigest } } : {}) });
91
+ const brokerSkills = config.broker ? await trainingBrokerSkills(options.context) : undefined;
91
92
  if (config.broker)
92
- await prepareTrainingBroker(config.broker, root, path.dirname(path.resolve(options.configPath)), false, config.slots);
93
+ await prepareTrainingBroker(config.broker, root, path.dirname(path.resolve(options.configPath)), false, config.slots, brokerSkills);
93
94
  if (options.dryRun)
94
95
  return { digest, dryRun: true };
95
96
  options.signal?.throwIfAborted();
@@ -152,7 +153,7 @@ export async function prepareTraining(options) {
152
153
  image = inspected.stdout.trim();
153
154
  }
154
155
  const mapped = mappedReceipt(digest, image, config);
155
- const preparedBroker = config.broker ? await prepareTrainingBroker(config.broker, root, staging, true, config.slots) : undefined;
156
+ const preparedBroker = config.broker ? await prepareTrainingBroker(config.broker, root, staging, true, config.slots, brokerSkills) : undefined;
156
157
  const launch = trainingContainerConfigSchema.parse({ version: launchVersion, slots: config.slots, dockerContext: config.dockerContext,
157
158
  inputs: inputs.map((input, index) => ({ source: staged[index], destination: input.destination })), output: { source: output, destination: "/run/training/output" }, auth, ...evaluatorConfig,
158
159
  ...(preparedBroker ? { broker: preparedBroker.launch } : {}) });
@@ -171,7 +172,7 @@ export async function prepareTraining(options) {
171
172
  throw error;
172
173
  }
173
174
  }
174
- const expectedBroker = config.broker ? await prepareTrainingBroker(config.broker, root, staging, false, config.slots) : undefined;
175
+ const expectedBroker = config.broker ? await prepareTrainingBroker(config.broker, root, staging, false, config.slots, brokerSkills) : undefined;
175
176
  const expectedLaunch = trainingContainerConfigSchema.parse({ version: launchVersion, slots: config.slots, dockerContext: config.dockerContext,
176
177
  inputs: inputs.map((input, index) => ({ source: staged[index], destination: input.destination })), output: { source: output, destination: "/run/training/output" }, auth, ...evaluatorConfig,
177
178
  ...(expectedBroker ? { broker: expectedBroker.launch } : {}) });
@@ -0,0 +1,25 @@
1
+ /** The slice of a leased broker section the declaration must agree with. */
2
+ export interface TrainingSubjectDeclaredBroker {
3
+ limits: {
4
+ maxRequests: number;
5
+ maxTokens: number;
6
+ timeoutMs: number;
7
+ };
8
+ workerConfigVariant: string;
9
+ }
10
+ /**
11
+ * Declaration-level policy a training subject must already satisfy in its
12
+ * resolved Spawnfile (inheritance included), checked before any compiler or
13
+ * subject runs.
14
+ *
15
+ * - Codex: the Daimon declaration states `codex_policy: workspace-no-network`
16
+ * and carries neither a broker slot nor `turn_limits`.
17
+ * - Grok: the declaration states all three `turn_limits`, each equal to the
18
+ * leased broker's registration limits, because a wake may only lower a
19
+ * registered limit and a trial must not run on a silently different budget;
20
+ * and the slot is the `clocked` worker variant, the only one that can hold a
21
+ * moment's shell clock.
22
+ *
23
+ * Any other runtime or engine is refused. Returns the engine it checked.
24
+ */
25
+ export declare const assertTrainingSubjectDeclaration: (projectRoot: string, agentId: string, broker?: TrainingSubjectDeclaredBroker) => Promise<"codex" | "grok">;
@@ -0,0 +1,52 @@
1
+ import { SpawnfileError } from "../../shared/index.js";
2
+ import { resolveDaimonEngine } from "../../runtime/daimon/config.js";
3
+ import { resolveDaimonGrokTurnLimits } from "../../runtime/daimon/grokTurnLimits.js";
4
+ import { loadTrainingProject } from "./plan/load.js";
5
+ import { selectTrainingAgent } from "./selection.js";
6
+ const refuse = (message) => { throw new SpawnfileError("validation_error", message); };
7
+ const LIMITS = [["maxRequests", "max_requests"], ["maxTokens", "max_tokens"], ["timeoutMs", "timeout_ms"]];
8
+ /**
9
+ * Declaration-level policy a training subject must already satisfy in its
10
+ * resolved Spawnfile (inheritance included), checked before any compiler or
11
+ * subject runs.
12
+ *
13
+ * - Codex: the Daimon declaration states `codex_policy: workspace-no-network`
14
+ * and carries neither a broker slot nor `turn_limits`.
15
+ * - Grok: the declaration states all three `turn_limits`, each equal to the
16
+ * leased broker's registration limits, because a wake may only lower a
17
+ * registered limit and a trial must not run on a silently different budget;
18
+ * and the slot is the `clocked` worker variant, the only one that can hold a
19
+ * moment's shell clock.
20
+ *
21
+ * Any other runtime or engine is refused. Returns the engine it checked.
22
+ */
23
+ export const assertTrainingSubjectDeclaration = async (projectRoot, agentId, broker) => {
24
+ const project = await loadTrainingProject(projectRoot);
25
+ const agents = project.compilePlan.nodes.filter((node) => node.value.kind === "agent");
26
+ const node = selectTrainingAgent(agents, agentId).value;
27
+ if (node.runtime.name !== "daimon")
28
+ return refuse(`Training subject ${agentId} must declare runtime daimon, not ${node.runtime.name}`);
29
+ const engine = resolveDaimonEngine(node);
30
+ const declared = resolveDaimonGrokTurnLimits(node.runtime.options.turn_limits);
31
+ if (engine === "codex") {
32
+ if (node.runtime.options.codex_policy !== "workspace-no-network") {
33
+ refuse(`Codex training subject ${agentId} requires explicit codex_policy: workspace-no-network on its Daimon declaration`);
34
+ }
35
+ if (broker !== undefined || declared !== undefined)
36
+ refuse(`Codex training subject ${agentId} takes no broker slot and no turn_limits`);
37
+ return engine;
38
+ }
39
+ if (engine !== "grok")
40
+ return refuse(`Training subject ${agentId} uses Daimon engine ${engine}; only codex and grok subjects train`);
41
+ if (!broker)
42
+ return refuse(`Grok training subject ${agentId} requires its leased broker slot`);
43
+ for (const [native, authored] of LIMITS) {
44
+ if (declared?.[native] !== broker.limits[native]) {
45
+ refuse(`Grok training subject ${agentId} turn_limits.${authored} (${String(declared?.[native] ?? "undeclared")}) differs from the broker limit ${broker.limits[native]}`);
46
+ }
47
+ }
48
+ if (broker.workerConfigVariant !== "clocked") {
49
+ refuse(`Grok training subject ${agentId} requires a broker slot provisioned and attested with workerConfigVariant: clocked`);
50
+ }
51
+ return engine;
52
+ };
@@ -0,0 +1,114 @@
1
+ import type { Socket } from "node:net";
2
+ import { DAIMON_GROK_ENGINE_BROKER, type DaimonGrokBrokerModel, type DaimonGrokBrokerReasoningEffort } from "../../runtime/daimon/contractManifest.js";
3
+ /** Paideia's slot freshness floor for this trial: the receipt must echo the nonce at no lower generation. */
4
+ export interface TrainingSubjectFreshness {
5
+ expectedNonce: string;
6
+ minGeneration: number;
7
+ }
8
+ /** The evaluator-created per-trial directories (Paideia `TrialPaths`); only Codex subjects live in them. */
9
+ export interface TrainingSubjectTrialRoots {
10
+ workspace: string;
11
+ runtimeHome: string;
12
+ }
13
+ export type TrainingSubjectEvaluatorRole = "run-root" | "context" | "sealed-inputs" | "judge-home" | "slot-ledger";
14
+ /** `paideia.daimon-native.launch.v2` `broker`, structurally `grokBrokerLaunchSchema` of Paideia 0.1.1. */
15
+ export interface TrainingSubjectBrokerSection {
16
+ projectionVersion: typeof DAIMON_GROK_ENGINE_BROKER.projectionVersion;
17
+ workerConfigVariant: "unclocked" | "clocked";
18
+ projectionSha256: string;
19
+ slot: number;
20
+ workerUid: number;
21
+ workerHomePath: string;
22
+ architecture: "arm64" | "x64";
23
+ usageLedgerPath: string;
24
+ seccompProfileSha256: string;
25
+ profileSha256: string;
26
+ denyPaths: string[];
27
+ limits: {
28
+ maxRequests: number;
29
+ maxTokens: number;
30
+ timeoutMs: number;
31
+ };
32
+ evaluatorPaths: {
33
+ role: TrainingSubjectEvaluatorRole;
34
+ path: string;
35
+ }[];
36
+ slotPreflightReceipt: string;
37
+ expectedNonce: string;
38
+ minGeneration: number;
39
+ }
40
+ export type TrainingSubjectAgent = {
41
+ id: string;
42
+ engine: "codex";
43
+ model: string;
44
+ } | {
45
+ id: string;
46
+ engine: "grok";
47
+ model: DaimonGrokBrokerModel;
48
+ reasoningEffort: DaimonGrokBrokerReasoningEffort;
49
+ };
50
+ export interface TrainingSubjectLease {
51
+ agent: TrainingSubjectAgent;
52
+ workspace: {
53
+ source: string;
54
+ target: string;
55
+ };
56
+ projections: {
57
+ role: "home" | "memory" | "transport";
58
+ source: string;
59
+ target: string;
60
+ }[];
61
+ credentials: {
62
+ source: string;
63
+ target: string;
64
+ }[];
65
+ broker?: TrainingSubjectBrokerSection;
66
+ /** Named slot roots: always `workspace`, `home` and `control`, plus engine-specific roots (Grok adds `context`). */
67
+ roots: Record<string, string> & {
68
+ workspace: string;
69
+ home: string;
70
+ control: string;
71
+ };
72
+ /** `control` is the worker control port; `moltnet` and `bridge` are provisioner-owned extras. */
73
+ ports: {
74
+ moltnet: number;
75
+ control: number;
76
+ bridge: number;
77
+ };
78
+ /** Recycled generation; Grok only. */
79
+ generation?: number;
80
+ /** Subject environment Spawnfile owns for this lease. */
81
+ env: Record<string, string>;
82
+ }
83
+ export interface LeaseTrainingSubjectOptions {
84
+ slot: number;
85
+ engine: string;
86
+ agentId: string;
87
+ /** The compiled Codex model; Grok's model comes from the broker declaration and, if given, must match it. */
88
+ model?: string;
89
+ /** Required for Grok, refused for Codex (which has no recycle). */
90
+ freshness?: TrainingSubjectFreshness;
91
+ /** Required for Codex; a Grok subject lives at the container's fixed slot roots. */
92
+ trialRoots?: TrainingSubjectTrialRoots;
93
+ signal: AbortSignal;
94
+ /** Resolves Daimon's public `@noopolis/daimon/runtime` export. */
95
+ loadDaimon?: () => Promise<unknown>;
96
+ /** Test seams: the supervisor connection and the declaration file. */
97
+ connect?: () => Socket;
98
+ declarationFile?: string;
99
+ slotsFile?: string;
100
+ }
101
+ /** The fixed read-only container leaf the training launch binds the Codex login to. */
102
+ export declare const TRAINING_CODEX_CREDENTIAL_INGRESS = "/run/paideia-auth/codex";
103
+ /**
104
+ * Lease one training slot for an engine-neutral subject and return everything
105
+ * `paideia.daimon-native.launch.v2` must carry about it.
106
+ *
107
+ * Spawnfile owns the container, its slots, the broker declaration and
108
+ * registration, the projection inputs, the recycle verb and the credential
109
+ * ingress paths, so an integration names an engine and an agent and never a
110
+ * container path. Grok recycles the slot with the evaluator's nonce and binds
111
+ * the supervisor's receipt; Codex derives its per-trial projections from the
112
+ * evaluator's own trial roots. Any other engine is refused.
113
+ */
114
+ export declare const leaseTrainingSubject: (options: LeaseTrainingSubjectOptions) => Promise<TrainingSubjectLease>;
@@ -0,0 +1,175 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { SpawnfileError } from "../../shared/index.js";
4
+ import { DAIMON_ENGINE_CREDENTIALS, DAIMON_GROK_ENGINE_BROKER } from "../../runtime/daimon/contractManifest.js";
5
+ import { parseTrainingBrokerDeclaration } from "./broker/declaration.js";
6
+ import { TRAINING_BROKER_DECLARATION_FILE, TRAINING_PAIDEIA_ROOT, TRAINING_SLOTS_FILE, trainingSlotDescriptors } from "./broker/paths.js";
7
+ import { loadDaimonProjectionModule, resolveTrainingSlotProjection } from "./broker/projection.js";
8
+ import { recycleTrainingSlot } from "./broker/recycleClient.js";
9
+ /** The fixed read-only container leaf the training launch binds the Codex login to. */
10
+ export const TRAINING_CODEX_CREDENTIAL_INGRESS = "/run/paideia-auth/codex";
11
+ const NONCE = /^[a-f0-9]{64}$/u;
12
+ const invalid = (message) => { throw new SpawnfileError("validation_error", message); };
13
+ const refused = (message) => { throw new SpawnfileError("runtime_error", message); };
14
+ const canonical = (label, value) => {
15
+ if (typeof value !== "string" || !path.posix.isAbsolute(value) || path.posix.normalize(value) !== value
16
+ || value.length < 2 || value.endsWith("/") || value.includes(","))
17
+ invalid(`Training subject ${label} must be a canonical absolute path`);
18
+ return value;
19
+ };
20
+ /**
21
+ * Lease one training slot for an engine-neutral subject and return everything
22
+ * `paideia.daimon-native.launch.v2` must carry about it.
23
+ *
24
+ * Spawnfile owns the container, its slots, the broker declaration and
25
+ * registration, the projection inputs, the recycle verb and the credential
26
+ * ingress paths, so an integration names an engine and an agent and never a
27
+ * container path. Grok recycles the slot with the evaluator's nonce and binds
28
+ * the supervisor's receipt; Codex derives its per-trial projections from the
29
+ * evaluator's own trial roots. Any other engine is refused.
30
+ */
31
+ export const leaseTrainingSubject = async (options) => {
32
+ options.signal.throwIfAborted();
33
+ if (!Number.isInteger(options.slot) || options.slot < 0)
34
+ invalid("Training subject lease requires a slot index");
35
+ if (options.engine === "grok")
36
+ return leaseGrokSubject(options);
37
+ if (options.engine === "codex")
38
+ return leaseCodexSubject(options);
39
+ return invalid(`Training subject engine ${String(options.engine)} is not supported; lease a codex or grok subject`);
40
+ };
41
+ const leaseGrokSubject = async (options) => {
42
+ const freshness = options.freshness;
43
+ if (!freshness || !NONCE.test(freshness.expectedNonce) || !Number.isSafeInteger(freshness.minGeneration) || freshness.minGeneration < 1) {
44
+ return invalid("A Grok training subject requires freshness with a 32-byte hex nonce and a generation floor of at least 1");
45
+ }
46
+ const declaration = parseTrainingBrokerDeclaration(JSON.parse(await readFile(options.declarationFile ?? TRAINING_BROKER_DECLARATION_FILE, "utf8")));
47
+ if (declaration.agentId !== options.agentId)
48
+ invalid(`The training broker declares ${declaration.agentId}, not ${options.agentId}`);
49
+ if (options.model !== undefined && options.model !== declaration.model)
50
+ invalid(`The training broker declares model ${declaration.model}, not ${options.model}`);
51
+ if (options.slot >= declaration.slots)
52
+ invalid(`Training slot ${options.slot} is outside the declared pool of ${declaration.slots}`);
53
+ const descriptor = trainingSlotDescriptors(declaration.slots)[options.slot];
54
+ // Resolved before the recycle: a Daimon whose contract drifted must refuse without wiping the slot.
55
+ const daimon = await loadDaimonProjectionModule(options.loadDaimon);
56
+ const { registration, projection, projectionSha256 } = await resolveTrainingSlotProjection(declaration, options.slot, daimon);
57
+ options.signal.throwIfAborted();
58
+ const result = await recycleTrainingSlot({ slot: options.slot, nonce: freshness.expectedNonce, signal: options.signal,
59
+ ...(options.connect ? { connect: options.connect } : {}) });
60
+ if (result.receipt !== descriptor.preflightReceipt)
61
+ refused("The slot supervisor published its receipt outside the leased slot root");
62
+ if (result.generation < freshness.minGeneration)
63
+ refused("The slot supervisor published a generation below the evaluator freshness floor");
64
+ options.signal.throwIfAborted();
65
+ const roots = {
66
+ workspace: descriptor.workspace, home: descriptor.runtimeHome, control: descriptor.state, workerHome: descriptor.workerHome,
67
+ usageLedger: descriptor.usageLedger, preflightReceipt: descriptor.preflightReceipt, context: TRAINING_PAIDEIA_ROOT
68
+ };
69
+ return {
70
+ agent: { id: declaration.agentId, engine: "grok", model: declaration.model, reasoningEffort: declaration.reasoningEffort },
71
+ workspace: { source: descriptor.workspace, target: descriptor.workspace },
72
+ // The projection digest binds exactly the slot workspace and runtime home; nothing else may be writable.
73
+ projections: [{ role: "home", source: descriptor.runtimeHome, target: descriptor.runtimeHome }],
74
+ credentials: [],
75
+ broker: {
76
+ projectionVersion: DAIMON_GROK_ENGINE_BROKER.projectionVersion,
77
+ workerConfigVariant: projection.workerConfigVariant,
78
+ projectionSha256,
79
+ slot: options.slot,
80
+ workerUid: descriptor.workerUid,
81
+ workerHomePath: descriptor.workerHome,
82
+ architecture: declaration.architecture,
83
+ usageLedgerPath: descriptor.usageLedger,
84
+ seccompProfileSha256: declaration.seccompProfileSha256,
85
+ // Verbatim provisioner values the digest binds: a deny list re-derived from evaluatorPaths would fork it.
86
+ profileSha256: registration.profileSha256,
87
+ denyPaths: [...registration.denyPaths],
88
+ limits: { ...declaration.limits },
89
+ evaluatorPaths: descriptor.evaluatorPaths.map((row) => ({ role: row.role, path: row.path })),
90
+ slotPreflightReceipt: descriptor.preflightReceipt,
91
+ expectedNonce: freshness.expectedNonce,
92
+ minGeneration: freshness.minGeneration
93
+ },
94
+ roots,
95
+ ports: { moltnet: descriptor.ports.moltnet, control: descriptor.ports.workerControl, bridge: descriptor.ports.bridge },
96
+ generation: result.generation,
97
+ env: { [DAIMON_GROK_ENGINE_BROKER.slotRouting.slotEnvironment]: String(options.slot) }
98
+ };
99
+ };
100
+ /** Indirect specifier: Daimon is an in-image runtime peer, never a Spawnfile source dependency. */
101
+ const loadDaimonRuntime = async () => {
102
+ const packageName = "@noopolis/daimon/runtime";
103
+ return import(packageName);
104
+ };
105
+ /** The Codex credential leaf Daimon reads, from its public export, refused unless it equals the vendored manifest. */
106
+ const codexCredentialRelativePath = async (load = loadDaimonRuntime) => {
107
+ let daimon;
108
+ try {
109
+ daimon = await load();
110
+ }
111
+ catch {
112
+ return refused("The training image must install @noopolis/daimon with its public /runtime export");
113
+ }
114
+ const relative = daimon?.ENGINE_CREDENTIAL_MATERIAL?.codex?.sourceRelativePath;
115
+ if (relative !== DAIMON_ENGINE_CREDENTIALS.codex.sourceRelativePath) {
116
+ return refused("The installed Daimon Codex credential material differs from this compiler's vendored manifest");
117
+ }
118
+ return relative;
119
+ };
120
+ /** Ports from the leased slot's own `slots.json` block (offsets 0=Moltnet, 1=worker control, 2=bridge), refused outside it. */
121
+ const leasedSlotPorts = async (slot, file) => {
122
+ let entry;
123
+ try {
124
+ const document = JSON.parse(await readFile(file, "utf8"));
125
+ entry = Array.isArray(document.slots) ? document.slots.find((row) => row?.index === slot) : undefined;
126
+ }
127
+ catch {
128
+ return refused("The training slots manifest is missing or unreadable");
129
+ }
130
+ const base = entry?.ports?.base, count = entry?.ports?.count;
131
+ if (!Number.isSafeInteger(base) || !Number.isSafeInteger(count) || base < 1 || count < 3) {
132
+ return refused(`Training slot ${slot} has no usable port block in the slots manifest`);
133
+ }
134
+ const first = base;
135
+ const offsets = { moltnet: first, control: first + 1, bridge: first + 2 };
136
+ if (Object.values(offsets).some((port) => port >= first + count || port > 65_535)) {
137
+ return refused(`Training slot ${slot} ports fall outside its leased block`);
138
+ }
139
+ return offsets;
140
+ };
141
+ const leaseCodexSubject = async (options) => {
142
+ if (options.freshness !== undefined)
143
+ invalid("A Codex training subject has no slot recycle; freshness does not apply");
144
+ if (typeof options.model !== "string" || !options.model.trim())
145
+ invalid("A Codex training subject requires its compiled model");
146
+ if (!options.trialRoots)
147
+ return invalid("A Codex training subject requires the evaluator's trial roots");
148
+ const workspace = canonical("workspace", options.trialRoots.workspace);
149
+ const runtimeHome = canonical("runtime home", options.trialRoots.runtimeHome);
150
+ const ports = await leasedSlotPorts(options.slot, options.slotsFile ?? TRAINING_SLOTS_FILE);
151
+ const relative = await codexCredentialRelativePath(options.loadDaimon);
152
+ options.signal.throwIfAborted();
153
+ // Codex's writable data sits beside the workspace, disjoint from the evaluator runtime and its controls.
154
+ const parent = path.posix.dirname(workspace);
155
+ const home = path.posix.join(parent, "home"), memory = path.posix.join(parent, "memory");
156
+ const transport = path.posix.join(runtimeHome, "native/moltnet");
157
+ const roots = { workspace, home, memory, transport, control: path.posix.join(runtimeHome, "native/root-control/acceptance"),
158
+ inputs: path.posix.join(runtimeHome, "native/root-control/inputs") };
159
+ const values = Object.values(roots);
160
+ if (new Set(values).size !== values.length || values.some((value) => value !== workspace && value.startsWith(`${workspace}/`))) {
161
+ invalid("Codex trial roots must not overlap the subject workspace");
162
+ }
163
+ return {
164
+ agent: { id: options.agentId, engine: "codex", model: options.model },
165
+ workspace: { source: workspace, target: workspace },
166
+ projections: [{ role: "home", source: home, target: home }, { role: "memory", source: memory, target: memory },
167
+ { role: "transport", source: transport, target: transport }],
168
+ // The read-only container leaf is the sole ingress; the child stages it into exactly this target
169
+ // (`stageTrainingAuth` from `spawnfile/auth`), never overwriting a renewed credential.
170
+ credentials: [{ source: TRAINING_CODEX_CREDENTIAL_INGRESS, target: path.posix.join(home, relative) }],
171
+ roots,
172
+ ports,
173
+ env: {}
174
+ };
175
+ };
@@ -1,14 +1,12 @@
1
1
  import type { ResolvedAgentNode } from "../../compiler/types.js";
2
2
  import type { RuntimeAdapter } from "../types.js";
3
3
  /**
4
- * Workspace skill roots per Daimon engine.
5
- *
6
- * Codex and AGY keep both CLI-engine roots. A brokered Grok worker loads no
7
- * workspace skill at all: Grok 1.0.34 discovers `.agents/skills` (never
8
- * `.codex/skills`) only in a trusted folder, and Daimon keeps the workspace
9
- * untrusted (root-owned empty `trusted_folders.toml`) and replaces the system
10
- * prompt, whose fixed text references no skill. Emitting either root would ship
11
- * files nothing reads, so none are emitted and the declaration is reported.
4
+ * Workspace skill roots per Daimon engine: every engine gets both CLI-engine
5
+ * roots. Codex and AGY read them in place. A brokered Grok worker cannot (its
6
+ * workspace stays untrusted), so root provisioning renders the same
7
+ * `.agents/skills` tree into the worker's `$GROK_HOME/skills`, which Grok loads
8
+ * as user-scoped skills, and the broker attests it against `skillsSha256`
9
+ * before every turn (`containerDaimonGrokWorkerProvisioning.ts`).
12
10
  */
13
- export declare const daimonSkillBaseDirectories: (node: ResolvedAgentNode) => readonly string[];
11
+ export declare const daimonSkillBaseDirectories: (_node: ResolvedAgentNode) => readonly string[];
14
12
  export declare const daimonAdapter: RuntimeAdapter;
@@ -1,6 +1,7 @@
1
1
  import { SpawnfileError } from "../../shared/index.js";
2
2
  import { CLI_ENGINE_SKILL_BASE_DIRECTORIES, createAgentCapabilities, createDiagnostic, createDocumentFiles, createSkillFiles } from "../common.js";
3
3
  import { parseEveryScheduleMs } from "../scheduleUtils.js";
4
+ import { daimonGrokSkillsError } from "./grokSkillsSupport.js";
4
5
  import { assertDaimonAgentOptions, assertDaimonInstructionBounds, createDaimonContainerTargets, daimonMemoryCapabilityFor, daimonMemorySelectionWarning, daimonMemoryVectorRecallWarning, DAIMON_CONFIG_FILE, DAIMON_CONTROL_PORT, DAIMON_ENGINES, resolveDaimonEngine } from "./config.js";
5
6
  import { resolveDaimonGrokModel } from "./grokModel.js";
6
7
  import { prepareDaimonRuntimeAuth } from "./runAuth.js";
@@ -58,23 +59,14 @@ const daimonWorkspaceRestrictionWarning = (node) => {
58
59
  + "the container boundary as this agent's only isolation.";
59
60
  };
60
61
  /**
61
- * Workspace skill roots per Daimon engine.
62
- *
63
- * Codex and AGY keep both CLI-engine roots. A brokered Grok worker loads no
64
- * workspace skill at all: Grok 1.0.34 discovers `.agents/skills` (never
65
- * `.codex/skills`) only in a trusted folder, and Daimon keeps the workspace
66
- * untrusted (root-owned empty `trusted_folders.toml`) and replaces the system
67
- * prompt, whose fixed text references no skill. Emitting either root would ship
68
- * files nothing reads, so none are emitted and the declaration is reported.
62
+ * Workspace skill roots per Daimon engine: every engine gets both CLI-engine
63
+ * roots. Codex and AGY read them in place. A brokered Grok worker cannot (its
64
+ * workspace stays untrusted), so root provisioning renders the same
65
+ * `.agents/skills` tree into the worker's `$GROK_HOME/skills`, which Grok loads
66
+ * as user-scoped skills, and the broker attests it against `skillsSha256`
67
+ * before every turn (`containerDaimonGrokWorkerProvisioning.ts`).
69
68
  */
70
- export const daimonSkillBaseDirectories = (node) => resolveDaimonEngine(node) === "grok" ? [] : CLI_ENGINE_SKILL_BASE_DIRECTORIES;
71
- const daimonGrokSkillWarning = (node) => {
72
- if (resolveDaimonEngine(node) !== "grok" || node.skills.length === 0)
73
- return undefined;
74
- return `Daimon Grok agent ${node.name} declares workspace skills (${node.skills.map((skill) => skill.name).sort().join(", ")}) `
75
- + "that its brokered worker never loads: Grok discovers project skills only in a trusted workspace, and Daimon keeps the "
76
- + "workspace untrusted and supplies instructions through the prompt. No skill files are emitted; move the guidance into the agent's docs.";
77
- };
69
+ export const daimonSkillBaseDirectories = (_node) => CLI_ENGINE_SKILL_BASE_DIRECTORIES;
78
70
  const daimonCodexPolicyError = (node) => {
79
71
  if (node.runtime.options.codex_policy === undefined)
80
72
  return undefined;
@@ -185,7 +177,7 @@ export const daimonAdapter = {
185
177
  const memoryVectorWarning = daimonMemoryVectorRecallWarning(node);
186
178
  const workspaceRestrictionWarning = daimonWorkspaceRestrictionWarning(node);
187
179
  const codexPolicyError = daimonCodexPolicyError(node);
188
- const grokSkillWarning = daimonGrokSkillWarning(node);
180
+ const grokSkillsError = daimonGrokSkillsError(node, resolveDaimonEngine(node));
189
181
  return {
190
182
  capabilities: createAgentCapabilities(node, {
191
183
  mcpOutcome: "supported",
@@ -202,8 +194,8 @@ export const daimonAdapter = {
202
194
  ...(memorySelectionWarning ? [createDiagnostic("warn", memorySelectionWarning)] : []),
203
195
  ...(memoryVectorWarning ? [createDiagnostic("warn", memoryVectorWarning)] : []),
204
196
  ...(workspaceRestrictionWarning ? [createDiagnostic("warn", workspaceRestrictionWarning)] : []),
205
- ...(grokSkillWarning ? [createDiagnostic("warn", grokSkillWarning)] : []),
206
- ...(codexPolicyError ? [createDiagnostic("error", codexPolicyError)] : [])
197
+ ...(codexPolicyError ? [createDiagnostic("error", codexPolicyError)] : []),
198
+ ...(grokSkillsError ? [createDiagnostic("error", grokSkillsError)] : [])
207
199
  ],
208
200
  files: [
209
201
  ...createDocumentFiles("workspace", node.docs),
@@ -218,6 +210,9 @@ export const daimonAdapter = {
218
210
  const codexPolicyError = daimonCodexPolicyError(node);
219
211
  if (codexPolicyError)
220
212
  throw new SpawnfileError("validation_error", codexPolicyError);
213
+ const grokSkillsError = daimonGrokSkillsError(node, resolveDaimonEngine(node));
214
+ if (grokSkillsError)
215
+ throw new SpawnfileError("validation_error", grokSkillsError);
221
216
  assertDaimonAgentOptions(nodeId, node);
222
217
  assertDaimonInstructionBounds(nodeId, node);
223
218
  },
@@ -1,5 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { CLI_ENGINE_SKILL_BASE_DIRECTORIES } from "../common.js";
3
+ import { daimonGrokSkillsError } from "./grokSkillsSupport.js";
4
+ import { daimonAgentSkillsFromResolved } from "./grokWorkerSkills.js";
3
5
  import { parseEveryScheduleMs } from "../scheduleUtils.js";
4
6
  import { SpawnfileError } from "../../shared/index.js";
5
7
  import { DAIMON_AGY_SUBSCRIPTION_REALM, DAIMON_ENGINE_CREDENTIALS, DAIMON_GROK_SUBSCRIPTION_REALM, DAIMON_GROK_TURN_USAGE_LEDGER } from "./contractManifest.js";
@@ -208,6 +210,15 @@ export const createDaimonContainerTargets = async (inputs) => {
208
210
  const grokModelByNodeId = Object.fromEntries(configAgents
209
211
  .filter((agent) => agent.engine.kind === "grok" && agent.engine.model !== undefined && agent.engine.reasoningEffort !== undefined)
210
212
  .map((agent) => [agent.id, { model: agent.engine.model, reasoningEffort: agent.engine.reasoningEffort }]));
213
+ // The compiled `.agents/skills` tree, in Daimon's engine-neutral form: root provisioning renders it into the worker's `$GROK_HOME/skills`.
214
+ for (const input of agents) {
215
+ const refused = daimonGrokSkillsError(input.value, engineByNodeId[input.id]);
216
+ if (refused)
217
+ throw new SpawnfileError("validation_error", refused);
218
+ }
219
+ const grokSkillsByNodeId = Object.fromEntries(agents
220
+ .filter((input) => engineByNodeId[input.id] === "grok" && input.value.skills.length > 0)
221
+ .map((input) => [input.id, daimonAgentSkillsFromResolved(input.value.skills)]));
211
222
  const hasAgy = configAgents.some((agent) => agent.engine.kind === "agy");
212
223
  const hasGrok = configAgents.some((agent) => agent.engine.kind === "grok");
213
224
  const agyRuntimeHomeMounts = configAgents
@@ -250,11 +261,12 @@ export const createDaimonContainerTargets = async (inputs) => {
250
261
  return [{
251
262
  agentPathsByNodeId: Object.fromEntries(configAgents.map((agent) => [agent.id, {
252
263
  workspacePath: agent.workspacePath, homePath: agent.runtimeHomePath,
253
- skillRoots: agent.engine.kind === "grok" ? [] : CLI_ENGINE_SKILL_BASE_DIRECTORIES.map((root) => path.posix.join(agent.workspacePath, path.posix.relative("workspace", root)))
264
+ skillRoots: CLI_ENGINE_SKILL_BASE_DIRECTORIES.map((root) => path.posix.join(agent.workspacePath, path.posix.relative("workspace", root)))
254
265
  }])),
255
266
  engineByNodeId,
256
267
  ...(hasGrok ? { grokModelByNodeId } : {}),
257
268
  ...(hasGrok && Object.keys(grokTurnLimitsByNodeId).length > 0 ? { grokTurnLimitsByNodeId } : {}),
269
+ ...(Object.keys(grokSkillsByNodeId).length > 0 ? { grokSkillsByNodeId } : {}),
258
270
  files: [
259
271
  ...agents.flatMap((input) => input.emittedFiles.map((file) => moveWorkspaceFile(file, input.slug))),
260
272
  { content: serializedConfig, path: DAIMON_CONFIG_FILE },
@@ -0,0 +1,20 @@
1
+ import type { ResolvedAgentNode } from "../../compiler/types.js";
2
+ /** The first Daimon whose broker accepts `skillsSha256` and renders `$GROK_HOME/skills` (noopolis/daimon-harness#55). */
3
+ export declare const DAIMON_GROK_SKILLS_MIN_VERSION = "0.2.2";
4
+ /**
5
+ * The Daimon package version of the runtime image `runtimes.yaml` pins, from
6
+ * its never-moved `<package version>-<short commit>` tag. Read synchronously
7
+ * because `preflightAgent` (`spawnfile validate`) is synchronous.
8
+ */
9
+ export declare const pinnedDaimonRuntimeVersion: (source?: string) => string;
10
+ /**
11
+ * Why a brokered Grok agent's declared skills cannot compile, or `undefined`.
12
+ *
13
+ * Two causes, both refused before anything is emitted: a skill Daimon's own
14
+ * parser would refuse (a name outside its component rule, a missing
15
+ * `SKILL.md`, a size past `AGENT_SKILLS_BOUNDS`), and a pinned runtime image
16
+ * older than 0.2.2, whose strict service parser rejects the `skillsSha256`
17
+ * key — so the broker, and every Grok agent of the organization, would fail
18
+ * to start.
19
+ */
20
+ export declare const daimonGrokSkillsError: (node: ResolvedAgentNode, engine: string, version?: () => string) => string | undefined;