@irisrun/agent 0.1.0 → 0.2.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/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @irisrun/agent
2
+
3
+ **The image toolchain that makes an agent a build artifact.** Parse an Agentfile,
4
+ resolve + embed + pin every contract by hash, and emit a content-addressed,
5
+ deterministic image — identical inputs produce an identical `imageDigest` — so a
6
+ session resumes from exactly the definition it was born under. This is the
7
+ library behind the `iris` CLI; host-side, zero external deps.
8
+
9
+ ## What it is
10
+
11
+ `parseAgentfileYaml` / `parseAgentfileJson` + `validateAgentfile` turn an
12
+ Agentfile into a checked model. `buildImage` resolves and pins each tool/bundle
13
+ contract, embeds content by hash, validates the capability profile, and computes
14
+ `imageDigest = sha256(canonicalize(image-minus-digest))` — the canonical image
15
+ excludes its own self-referential digest, so the digest is reproducible
16
+ (`computeImageDigest`, `canonicalImageOf`). `writeOciLayout` / `readOciLayout`
17
+ serialize the image as a local, files-only OCI layout. `verifyImage` re-checks
18
+ every content hash, re-resolves every contract, and recomputes the digest —
19
+ throwing **loudly** on any drift, never silently. `latestRecord`,
20
+ `governingDigest`, and `migrateDefinition` pin a session to its image digest and
21
+ migrate a live session `from`→`to` at a turn boundary, with **zero** engine change.
22
+
23
+ ## Use it
24
+
25
+ ```sh
26
+ iris build --file ./my-agent/agent.yaml --out ./image
27
+ iris inspect ./image
28
+ iris verify ./image
29
+ ```
30
+
31
+ See **[docs/Your first agent](../../docs/first-agent.md)** and
32
+ **[docs/Architecture](../../docs/architecture.md)**.
33
+
34
+ ---
35
+ Part of [Iris](../../README.md) — own, portable, verifiable state.
@@ -27,7 +27,10 @@ export interface AgentfileModel {
27
27
  workspace?: string;
28
28
  network: string;
29
29
  };
30
+ secrets?: string[];
31
+ environment?: Record<string, string>;
30
32
  }
33
+ export declare function isEnvName(s: string): boolean;
31
34
  /** Parse Agentfile JSON text into a validated model (throws loudly on bad JSON or shape). */
32
35
  export declare function parseAgentfileJson(text: string): AgentfileModel;
33
36
  /**
@@ -35,7 +38,7 @@ export declare function parseAgentfileJson(text: string): AgentfileModel;
35
38
  * (shape, required fields) and enforces the content-vs-contract split: a tool /
36
39
  * connection entry is REJECTED if it carries an inline-behavior field
37
40
  * (code/script/source) or a ref whose scheme is not mcp/grpc/subprocess
38
- * (ADR-0005 — no behavior in the manifest). Throws loudly; never coerces.
41
+ * (no behavior in the manifest). Throws loudly; never coerces.
39
42
  */
40
43
  export declare function validateAgentfile(raw: unknown): AgentfileModel;
41
44
  /** Content paths embedded by hash: instructions, skills, then sandbox.workspace (if any). */
package/dist/agentfile.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // A contract ref must use one of these schemes; anything else (incl. an inline
2
- // `code`/`script`/`source` field) is inlined behavior and is rejected (ADR-0005).
2
+ // `code`/`script`/`source` field) is inlined behavior and is rejected.
3
3
  const CONTRACT_SCHEMES = ["mcp", "grpc", "subprocess"];
4
4
  const INLINE_BEHAVIOR_FIELDS = ["code", "script", "source"];
5
5
  // `requires` (the capability profile) is strict-when-present so the runtime
@@ -8,6 +8,14 @@ const INLINE_BEHAVIOR_FIELDS = ["code", "script", "source"];
8
8
  // boolean. (Initiative 20260620-agentfile-schema — these were untyped before.)
9
9
  const TOOL_LOCALITIES = ["in-process", "local", "remote"];
10
10
  const BOOLEAN_CAPS = ["long_running", "local_subprocess", "filesystem", "websockets"];
11
+ // A POSIX-style environment-variable NAME: a letter/underscore then letters/
12
+ // digits/underscores. Used for both `secrets` entries and `environment` keys here
13
+ // AND re-exported (index.ts) so the CLI env resolver enforces the SAME shape —
14
+ // authoring-time and runtime agree on what a valid name is.
15
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
16
+ export function isEnvName(s) {
17
+ return ENV_NAME_RE.test(s);
18
+ }
11
19
  /** Parse Agentfile JSON text into a validated model (throws loudly on bad JSON or shape). */
12
20
  export function parseAgentfileJson(text) {
13
21
  let raw;
@@ -24,7 +32,7 @@ export function parseAgentfileJson(text) {
24
32
  * (shape, required fields) and enforces the content-vs-contract split: a tool /
25
33
  * connection entry is REJECTED if it carries an inline-behavior field
26
34
  * (code/script/source) or a ref whose scheme is not mcp/grpc/subprocess
27
- * (ADR-0005 — no behavior in the manifest). Throws loudly; never coerces.
35
+ * (no behavior in the manifest). Throws loudly; never coerces.
28
36
  */
29
37
  export function validateAgentfile(raw) {
30
38
  const o = asObject(raw, "Agentfile");
@@ -71,6 +79,17 @@ export function validateAgentfile(raw) {
71
79
  throw new Error("Agentfile: sandbox.workspace must be a string");
72
80
  sandboxOut.workspace = sandbox.workspace;
73
81
  }
82
+ // Env/secrets — OPTIONAL; validated strictly, OMITTED when absent (canonicalize
83
+ // throws on undefined AND omission keeps existing digests byte-identical).
84
+ const secretsOut = validateSecrets(o.secrets);
85
+ const environmentOut = validateEnvironment(o.environment);
86
+ if (secretsOut !== undefined && environmentOut !== undefined) {
87
+ for (const name of secretsOut) {
88
+ if (Object.prototype.hasOwnProperty.call(environmentOut, name)) {
89
+ throw new Error(`Agentfile: "${name}" is declared as both a secret and an environment literal — a name must be one or the other`);
90
+ }
91
+ }
92
+ }
74
93
  return {
75
94
  apiVersion: "iris/v1",
76
95
  kind: "Agent",
@@ -83,8 +102,58 @@ export function validateAgentfile(raw) {
83
102
  harness: harnessOut,
84
103
  requires: requires,
85
104
  sandbox: sandboxOut,
105
+ ...(secretsOut !== undefined ? { secrets: secretsOut } : {}),
106
+ ...(environmentOut !== undefined ? { environment: environmentOut } : {}),
86
107
  };
87
108
  }
109
+ // Validate the OPTIONAL `secrets` (NAMES of required runtime secrets). Returns the
110
+ // validated array, or undefined when absent (omitted from the model → digest-stable).
111
+ // Order is author-significant and digest-affecting — NOT sorted.
112
+ function validateSecrets(v) {
113
+ if (v === undefined)
114
+ return undefined;
115
+ if (!Array.isArray(v)) {
116
+ throw new Error('Agentfile: "secrets" must be an array of environment-variable names');
117
+ }
118
+ const seen = new Set();
119
+ for (const entry of v) {
120
+ if (typeof entry !== "string" || !isEnvName(entry)) {
121
+ throw new Error(`Agentfile: secrets[] entry ${JSON.stringify(entry)} is not a valid env-var name (a letter/underscore then letters/digits/underscores)`);
122
+ }
123
+ if (seen.has(entry)) {
124
+ throw new Error(`Agentfile: duplicate secret name "${entry}"`);
125
+ }
126
+ seen.add(entry);
127
+ }
128
+ return v;
129
+ }
130
+ // Validate the OPTIONAL `environment` (literal NON-secret config). Scalar values
131
+ // (string/number/boolean) are coerced to strings HERE — env vars are inherently
132
+ // strings, so JSON `"3"`, YAML `3`, and YAML `"3"` all produce the SAME model and
133
+ // therefore the SAME imageDigest. null/object/array values are rejected loudly.
134
+ // Returns the coerced map, or undefined when absent.
135
+ function validateEnvironment(v) {
136
+ if (v === undefined)
137
+ return undefined;
138
+ const o = asObject(v, "environment");
139
+ const out = {};
140
+ for (const [key, val] of Object.entries(o)) {
141
+ if (!isEnvName(key)) {
142
+ throw new Error(`Agentfile: environment key ${JSON.stringify(key)} is not a valid env-var name (a letter/underscore then letters/digits/underscores)`);
143
+ }
144
+ if (typeof val === "string") {
145
+ out[key] = val;
146
+ }
147
+ else if (typeof val === "number" || typeof val === "boolean") {
148
+ out[key] = String(val);
149
+ }
150
+ else {
151
+ const kind = val === null ? "null" : Array.isArray(val) ? "array" : typeof val;
152
+ throw new Error(`Agentfile: environment.${key} must be a string (got ${kind})`);
153
+ }
154
+ }
155
+ return out;
156
+ }
88
157
  // Strict-when-present validation of the capability profile (`requires`), so the
89
158
  // runtime agrees with the published JSON schema. Unknown keys are still ignored
90
159
  // (retained in the model) — only the KNOWN fields are type-checked when present.
@@ -119,7 +188,7 @@ function requireStringArray(o, key) {
119
188
  }
120
189
  return v;
121
190
  }
122
- // Validate a tools/connections array, rejecting inlined behavior (ADR-0005).
191
+ // Validate a tools/connections array, rejecting inlined behavior.
123
192
  function requireRefArray(o, key) {
124
193
  const v = o[key];
125
194
  if (!Array.isArray(v)) {
@@ -129,7 +198,7 @@ function requireRefArray(o, key) {
129
198
  const e = asObject(entry, `${key}[${i}]`);
130
199
  for (const field of INLINE_BEHAVIOR_FIELDS) {
131
200
  if (field in e) {
132
- throw new Error(`Agentfile: ${key}[${i}] carries inline behavior ("${field}") — tools are contracts referenced by digest, not inlined code (ADR-0005)`);
201
+ throw new Error(`Agentfile: ${key}[${i}] carries inline behavior ("${field}") — tools are contracts referenced by digest, not inlined code`);
133
202
  }
134
203
  }
135
204
  const ref = e.ref;
package/dist/bundle.js CHANGED
@@ -1,9 +1,9 @@
1
- // Bundle resolution + digest (spec §4.2, ADR-0004) — the M6 strengthening of the
2
- // M4 tactic pin. A tactic BUNDLE (e.g. `@irisrun/bundle-coding`) is distributed like
1
+ // Bundle resolution + digest — the strengthening of the
2
+ // tactic pin. A tactic BUNDLE (e.g. `@irisrun/bundle-coding`) is distributed like
3
3
  // a tool contract: an Agentfile `harness.bundle` ref (e.g. "iris/coding@^1")
4
- // resolves — via an injected BundleResolver, mirroring the M4 RegistryResolver —
4
+ // resolves — via an injected BundleResolver, mirroring the RegistryResolver —
5
5
  // to a concrete BundleDefinition, and the image pins `bundleDigest(def)` (a real
6
- // content digest over the BEHAVIOR SURFACE) instead of the M4 sha256Hex(id)
6
+ // content digest over the BEHAVIOR SURFACE) instead of the sha256Hex(id)
7
7
  // placeholder. The digest is STABLE across a floating `location` (re-resolve by
8
8
  // stable ref, not by location — [[lrn-resolve-by-stable-ref-not-floating-location]])
9
9
  // yet detects any change to the behavior surface (id/version/seams/...).
@@ -13,7 +13,7 @@ import { canonicalize } from "@irisrun/core";
13
13
  // The behavior surface the digest is computed over: the full definition MINUS the
14
14
  // floating `location` (and any undefined fields, which canonicalize rejects). Two
15
15
  // definitions that differ only in `location` produce the SAME surface → the SAME
16
- // digest (the ADR-0004 float-impl property); any behavior-surface change differs.
16
+ // digest (the float-impl property); any behavior-surface change differs.
17
17
  function behaviorSurface(def) {
18
18
  const surface = {};
19
19
  for (const [k, v] of Object.entries(def)) {
package/dist/image.d.ts CHANGED
@@ -34,6 +34,8 @@ export interface ImageInspection {
34
34
  digest: string;
35
35
  }>;
36
36
  capabilities: CapabilityProfile;
37
+ secrets?: string[];
38
+ environment?: Record<string, string>;
37
39
  }
38
40
  /** Human-readable resolved intent of an image (what `iris inspect` prints). */
39
41
  export declare function inspectImage(image: AgentImage): ImageInspection;
package/dist/image.js CHANGED
@@ -1,4 +1,4 @@
1
- // Image build (spec §3.5): resolve + pin → embed content by hash → compute the
1
+ // Image build: resolve + pin → embed content by hash → compute the
2
2
  // content-addressed, deterministic imageDigest = sha256(canonicalize(canonical
3
3
  // image)). The canonical image EXCLUDES the self-referential imageDigest field;
4
4
  // content values are base64 STRINGS (canonicalize rejects Buffer/Uint8Array) and
@@ -60,7 +60,7 @@ export async function buildImage(model, opts) {
60
60
  if (model.harness.bundle !== undefined) {
61
61
  const id = model.harness.bundle;
62
62
  if (opts.resolveBundle !== undefined) {
63
- // M6: resolve the bundle ref to a definition and pin its REAL content digest.
63
+ // Resolve the bundle ref to a definition and pin its REAL content digest.
64
64
  // The id stays the STABLE Agentfile ref (re-resolved by verify, location floats).
65
65
  const def = await opts.resolveBundle.resolve(id);
66
66
  if (def === null) {
@@ -69,7 +69,7 @@ export async function buildImage(model, opts) {
69
69
  tactics.bundle = { id, digest: bundleDigest(def) };
70
70
  }
71
71
  else {
72
- // Back-compat (every M4 path): keep the sha256Hex(id) placeholder unchanged.
72
+ // Back-compat: keep the sha256Hex(id) placeholder unchanged.
73
73
  tactics.bundle = { id, digest: sha256Hex(id) };
74
74
  }
75
75
  }
@@ -100,9 +100,11 @@ export function inspectImage(image) {
100
100
  content: image.lock.content,
101
101
  tactics: image.lock.tactics,
102
102
  capabilities: image.lock.capabilities,
103
+ ...(image.agentfile.secrets !== undefined ? { secrets: image.agentfile.secrets } : {}),
104
+ ...(image.agentfile.environment !== undefined ? { environment: image.agentfile.environment } : {}),
103
105
  };
104
106
  }
105
- // --- OCI image layout (local, files-only — spec §3.5) -------------------------
107
+ // --- OCI image layout (local, files-only) -------------------------
106
108
  // Real registry push/pull (+ cosign) is the manual smoke; this is the install-free
107
109
  // path. Shape: `oci-layout` + `index.json` + `blobs/sha256/<hex>`.
108
110
  const IRIS_IMAGE_MEDIA_TYPE = "application/vnd.iris.agent.image+json";
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare const PACKAGE = "@irisrun/agent";
2
- export { parseAgentfileJson, validateAgentfile, contentPaths, contractRefs, refScheme, } from "./agentfile.js";
2
+ export { parseAgentfileJson, validateAgentfile, contentPaths, contractRefs, refScheme, isEnvName, } from "./agentfile.js";
3
3
  export type { AgentfileModel, CapabilityProfile, ToolRef, } from "./agentfile.js";
4
4
  export { parseAgentfileYaml, parseYamlValue } from "./yaml.js";
5
5
  export { AGENTFILE_SCHEMA, agentfileSchemaJson, checkAgainstSchema } from "./schema.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @irisrun/agent — public surface (host-side; zero external deps).
2
2
  export const PACKAGE = "@irisrun/agent";
3
- export { parseAgentfileJson, validateAgentfile, contentPaths, contractRefs, refScheme, } from "./agentfile.js";
3
+ export { parseAgentfileJson, validateAgentfile, contentPaths, contractRefs, refScheme, isEnvName, } from "./agentfile.js";
4
4
  export { parseAgentfileYaml, parseYamlValue } from "./yaml.js";
5
5
  export { AGENTFILE_SCHEMA, agentfileSchemaJson, checkAgainstSchema } from "./schema.js";
6
6
  export { makeLocalResolver, refBase } from "./resolver.js";
package/dist/lock.d.ts CHANGED
@@ -31,7 +31,7 @@ export interface Lock {
31
31
  */
32
32
  export declare function resolveLockTools(refs: ToolRef[], resolver: RegistryResolver): Promise<LockTool[]>;
33
33
  /**
34
- * Validate the capability profile against the resolved tools (spec §3.3 step 4),
34
+ * Validate the capability profile against the resolved tools,
35
35
  * loudly (no silent inconsistency): a `remote` locality forbids subprocess tools,
36
36
  * and any subprocess tool requires `local_subprocess: true`.
37
37
  */
package/dist/lock.js CHANGED
@@ -1,6 +1,6 @@
1
- // The lockfile (spec §3.4) — pins model + tools/connections (by contractDigest) +
1
+ // The lockfile — pins model + tools/connections (by contractDigest) +
2
2
  // tactics + capabilities + the embedded content hashes. `imageDigest` is filled by
3
- // the image build (§3.5). This module owns the tool-resolution + pinning + the
3
+ // the image build. This module owns the tool-resolution + pinning + the
4
4
  // capability validation; Task 4 completes content/model/tactics. Host-side.
5
5
  import { contractDigest } from "@irisrun/tools";
6
6
  /**
@@ -31,7 +31,7 @@ export async function resolveLockTools(refs, resolver) {
31
31
  return out;
32
32
  }
33
33
  /**
34
- * Validate the capability profile against the resolved tools (spec §3.3 step 4),
34
+ * Validate the capability profile against the resolved tools,
35
35
  * loudly (no silent inconsistency): a `remote` locality forbids subprocess tools,
36
36
  * and any subprocess tool requires `local_subprocess: true`.
37
37
  */
package/dist/pin.d.ts CHANGED
@@ -22,7 +22,7 @@ export interface MigrateOptions {
22
22
  }
23
23
  /**
24
24
  * Migrate a LIVE session's pinned definition `from`→`to` at a turn boundary
25
- * (ADR-0004 hold-and-migrate). Appends an `upgraded {from,to,atTurn}` marker
25
+ * (hold-and-migrate). Appends an `upgraded {from,to,atTurn}` marker
26
26
  * stamped with `defDigest: to`; subsequent turns run with `defDigest = to`. Refuses
27
27
  * LOUDLY when the session has not started, is mid-turn, or its governing digest is
28
28
  * not `from`. `atTurn` = the boundary journal sequence (the engine emits no
package/dist/pin.js CHANGED
@@ -1,4 +1,4 @@
1
- // Session pinning + definition migration (spec §3.7, ADR-0002/0004) — ZERO engine
1
+ // Session pinning + definition migration — ZERO engine
2
2
  // change. A session pins the image digest as its governing `defDigest` (the engine
3
3
  // already stamps every record). Pinning/migration ride the EXISTING per-record
4
4
  // defDigest + the `upgraded` marker; this module uses only the StateStore port +
@@ -32,7 +32,7 @@ export async function governingDigest(store, sessionId) {
32
32
  const TURN_TERMINAL_MARKERS = new Set(["wait", "finish"]);
33
33
  /**
34
34
  * Migrate a LIVE session's pinned definition `from`→`to` at a turn boundary
35
- * (ADR-0004 hold-and-migrate). Appends an `upgraded {from,to,atTurn}` marker
35
+ * (hold-and-migrate). Appends an `upgraded {from,to,atTurn}` marker
36
36
  * stamped with `defDigest: to`; subsequent turns run with `defDigest = to`. Refuses
37
37
  * LOUDLY when the session has not started, is mid-turn, or its governing digest is
38
38
  * not `from`. `atTurn` = the boundary journal sequence (the engine emits no
@@ -7,6 +7,6 @@ export declare function refBase(ref: string): string;
7
7
  /**
8
8
  * A resolver over an in-memory `refBase → ToolContract` map (install-free). Both
9
9
  * `mcp://r/x@^2` and `mcp://r/x@^3` resolve via the shared base `mcp://r/x`,
10
- * modelling "pin the contract, float the implementation" (ADR-0004).
10
+ * modelling "pin the contract, float the implementation".
11
11
  */
12
12
  export declare function makeLocalResolver(map: Record<string, ToolContract>): RegistryResolver;
package/dist/resolver.js CHANGED
@@ -7,7 +7,7 @@ export function refBase(ref) {
7
7
  /**
8
8
  * A resolver over an in-memory `refBase → ToolContract` map (install-free). Both
9
9
  * `mcp://r/x@^2` and `mcp://r/x@^3` resolve via the shared base `mcp://r/x`,
10
- * modelling "pin the contract, float the implementation" (ADR-0004).
10
+ * modelling "pin the contract, float the implementation".
11
11
  */
12
12
  export function makeLocalResolver(map) {
13
13
  return {
package/dist/schema.js CHANGED
@@ -17,7 +17,7 @@ export const AGENTFILE_SCHEMA = {
17
17
  $schema: "https://json-schema.org/draft/2020-12/schema",
18
18
  $id: "https://iris.run/schema/agentfile/v1.json",
19
19
  title: "Iris Agentfile (iris/v1, kind Agent)",
20
- description: "An Iris Agentfile: a declarative RECIPE that references content (embedded by hash) and tool/connection contracts (pinned by digest). It carries NO executable behavior (ADR-0005).",
20
+ description: "An Iris Agentfile: a declarative RECIPE that references content (embedded by hash) and tool/connection contracts (pinned by digest). It carries NO executable behavior.",
21
21
  type: "object",
22
22
  required: [
23
23
  "apiVersion",
@@ -66,6 +66,24 @@ export const AGENTFILE_SCHEMA = {
66
66
  harness: { $ref: "#/$defs/harness" },
67
67
  requires: { $ref: "#/$defs/capabilityProfile" },
68
68
  sandbox: { $ref: "#/$defs/sandbox" },
69
+ // Env/secrets (initiative 20260620-agentfile-env-secrets). `secrets` = NAMES
70
+ // of required runtime secrets (VALUES are supplied at run time, never in the
71
+ // manifest). `environment` = literal NON-secret config defaults.
72
+ // AGREEMENT-CORPUS FOOTGUN: the zero-dep checker can express only what is
73
+ // below — secrets is an array of pattern-valid strings, and environment is an
74
+ // object. It CANNOT express secrets uniqueness, secrets/environment overlap,
75
+ // environment KEY patterns, or environment VALUE typing. Those are runtime-only
76
+ // (validateAgentfile) and must NOT be added to the T3 agreement corpus — the
77
+ // schema accepts them, so a corpus entry would make the two surfaces disagree.
78
+ secrets: {
79
+ type: "array",
80
+ items: { type: "string", minLength: 1, pattern: "^[A-Za-z_][A-Za-z0-9_]*$" },
81
+ description: "Names of required runtime secrets — values are supplied at run time, never stored in the manifest/image.",
82
+ },
83
+ environment: {
84
+ type: "object",
85
+ description: "Literal NON-secret config defaults (string values). Object-ness is the shared constraint with the runtime; value coercion/typing is runtime-only.",
86
+ },
69
87
  },
70
88
  additionalProperties: true,
71
89
  $defs: {
@@ -79,7 +97,7 @@ export const AGENTFILE_SCHEMA = {
79
97
  pattern: "^(mcp|grpc|subprocess)://",
80
98
  description: "Contract ref. Scheme must be mcp, grpc, or subprocess.",
81
99
  },
82
- // ADR-0005: a tool/connection is a contract referenced by digest, never
100
+ // A tool/connection is a contract referenced by digest, never
83
101
  // inlined code. A present code/script/source field validates against the
84
102
  // boolean `false` subschema → rejected.
85
103
  code: false,
package/dist/verify.js CHANGED
@@ -1,4 +1,4 @@
1
- // Integrity verification (spec §3.6, ADR-0006 §4) — throws LOUDLY on any failure
1
+ // Integrity verification — throws LOUDLY on any failure
2
2
  // (no silent corruption, [[lrn-no-silent-policy-widening]]). Checks, in order:
3
3
  // (1) every embedded content hash matches its bytes; (2) every tool contractDigest
4
4
  // is still resolvable + unchanged; (3) the recomputed imageDigest equals the
@@ -20,7 +20,7 @@ export async function verifyImage(image, opts) {
20
20
  }
21
21
  }
22
22
  // 2. every tool contract is still resolvable (BY ITS STABLE ref — not location,
23
- // which floats per ADR-0004) and its digest unchanged
23
+ // which floats) and its digest unchanged
24
24
  for (const tool of image.lock.tools) {
25
25
  const contract = await opts.resolver.resolve(tool.ref);
26
26
  if (contract === null) {
@@ -37,11 +37,11 @@ export async function verifyImage(image, opts) {
37
37
  if (recomputed !== image.lock.imageDigest) {
38
38
  throw new Error(`verify: imageDigest mismatch — stored ${image.lock.imageDigest}, recomputed ${recomputed}`);
39
39
  }
40
- // 4. (M6, NET-NEW) re-resolve the pinned tactic bundle by its STABLE id/ref (NOT
41
- // a floating location, ADR-0004), recompute bundleDigest, and assert it equals
40
+ // 4. (NET-NEW) re-resolve the pinned tactic bundle by its STABLE id/ref (NOT
41
+ // a floating location), recompute bundleDigest, and assert it equals
42
42
  // the pinned digest. This catches a CONTENT-tampered bundle whose pinned lock
43
43
  // digest is left unchanged — invisible to the imageDigest check above. Skipped
44
- // entirely when no resolveBundle is injected (M4 back-compat).
44
+ // entirely when no resolveBundle is injected (back-compat).
45
45
  if (opts.resolveBundle !== undefined) {
46
46
  const pinned = image.lock.tactics.bundle;
47
47
  if (pinned !== undefined) {
package/dist/yaml.js CHANGED
@@ -125,8 +125,16 @@ function looksScalar(item) {
125
125
  return !/^[A-Za-z0-9_.-]+:\s/.test(item) && !/^[A-Za-z0-9_.-]+:$/.test(item);
126
126
  }
127
127
  function parseScalar(s, lineNo) {
128
+ // The ONLY supported flow collections are the EMPTY literals `[]` and `{}` — so a
129
+ // no-skills / no-connections agent (very common) is authorable in YAML, which the
130
+ // block `- ` / `key:` syntax cannot express. NON-empty flow stays rejected (no
131
+ // general flow support). Initiative 20260620-agentfile-env-secrets.
132
+ if (s === "[]")
133
+ return [];
134
+ if (s === "{}")
135
+ return {};
128
136
  if (s.startsWith("[") || s.startsWith("{")) {
129
- throw new Error(`Agentfile YAML: flow collections ([..]/{..}) are unsupported (line ${lineNo})`);
137
+ throw new Error(`Agentfile YAML: non-empty flow collections ([..]/{..}) are unsupported — only the empty literals [] and {} (line ${lineNo})`);
130
138
  }
131
139
  if (s.startsWith("&") || s.startsWith("*")) {
132
140
  throw new Error(`Agentfile YAML: anchors/aliases (&/*) are unsupported (line ${lineNo})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irisrun/agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Iris agent image toolchain — Agentfile model + parsers, builder (resolve/embed/pin/validate), content-addressed inspectable image + OCI layout, lockfile, integrity verify, and session pinning + definition migration. Host-side, zero external deps.",
6
6
  "exports": {
@@ -11,8 +11,8 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@irisrun/core": "^0.1.0",
15
- "@irisrun/tools": "^0.1.0"
14
+ "@irisrun/core": "^0.2.0",
15
+ "@irisrun/tools": "^0.2.0"
16
16
  },
17
17
  "license": "MIT",
18
18
  "engines": {