@irisrun/host 0.1.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.
@@ -0,0 +1,33 @@
1
+ import type { StateStore, Scheduler, LogicalClock, Program, PerformerRegistry, TurnOutcome, JournalRecord, Json } from "@irisrun/core";
2
+ import type { CapabilityProfile } from "@irisrun/agent";
3
+ export interface HostAdapter {
4
+ name: string;
5
+ capabilities: CapabilityProfile;
6
+ store: StateStore;
7
+ scheduler: Scheduler;
8
+ }
9
+ export interface RunTurnOnOptions<S extends Json> {
10
+ sessionId: string;
11
+ defDigest: string;
12
+ program: Program<S>;
13
+ performers: PerformerRegistry;
14
+ clock: LogicalClock;
15
+ holderId?: string;
16
+ assertReplay?: boolean;
17
+ snapshotThreshold?: number;
18
+ keepHistory?: boolean;
19
+ maxStepsPerTurn?: number;
20
+ onWarn?: (message: string) => void;
21
+ onRecord?: (record: JournalRecord) => void;
22
+ }
23
+ /** Run one turn on `adapter`'s store + scheduler. Same image (defDigest) + program
24
+ * on any host → the engine's deterministic replay does the rest. */
25
+ export declare function runTurnOn<S extends Json>(adapter: HostAdapter, opts: RunTurnOnOptions<S>): Promise<TurnOutcome<S>>;
26
+ /**
27
+ * Tool/host-level capability check (ADR-0008): for every capability the image
28
+ * REQUIRES (`requires[k] === true`), the host must PROVIDE it (`capabilities[k]
29
+ * === true`). `undefined`/`false` on the host means NOT satisfied — never silently
30
+ * widened (cf. the secure-floor posture). Refuses LOUDLY, naming the gaps; the
31
+ * full host capability-diff gate is deferred to M6.
32
+ */
33
+ export declare function checkHostCapabilities(requires: CapabilityProfile, capabilities: CapabilityProfile, hostName?: string): void;
@@ -0,0 +1,53 @@
1
+ // HostAdapter (Spec 04 §3): a host is just {name, capabilities, store, scheduler}.
2
+ // `runTurnOn` runs a turn ON a host's store+scheduler — the host convenience that
3
+ // makes "the SAME image, a DIFFERENT host" explicit. It REPLACES the framework's
4
+ // enterTurn(sessionId,event) member (there is nothing to replace — no core type);
5
+ // it is a thin call into the engine's runTurn with the adapter's ports injected.
6
+ // `checkHostCapabilities` is the tool/host-level ADR-0008 refusal (the FULL host
7
+ // capability-diff gate stays deferred to M6). Host-side; core stays pure.
8
+ import { runTurn } from "@irisrun/core";
9
+ /** Run one turn on `adapter`'s store + scheduler. Same image (defDigest) + program
10
+ * on any host → the engine's deterministic replay does the rest. */
11
+ export async function runTurnOn(adapter, opts) {
12
+ return runTurn({
13
+ store: adapter.store,
14
+ scheduler: adapter.scheduler,
15
+ clock: opts.clock,
16
+ program: opts.program,
17
+ performers: opts.performers,
18
+ defDigest: opts.defDigest,
19
+ holderId: opts.holderId ?? adapter.name,
20
+ assertReplay: opts.assertReplay,
21
+ snapshotThreshold: opts.snapshotThreshold,
22
+ keepHistory: opts.keepHistory,
23
+ maxStepsPerTurn: opts.maxStepsPerTurn,
24
+ onWarn: opts.onWarn,
25
+ onRecord: opts.onRecord,
26
+ }, opts.sessionId);
27
+ }
28
+ // The boolean capability keys (tool_locality is a profile STRING, not a gateable
29
+ // boolean, so it is not part of this refusal — the full degrade/refuse matrix is M6).
30
+ const BOOLEAN_CAPS = [
31
+ "long_running",
32
+ "local_subprocess",
33
+ "filesystem",
34
+ "websockets",
35
+ ];
36
+ /**
37
+ * Tool/host-level capability check (ADR-0008): for every capability the image
38
+ * REQUIRES (`requires[k] === true`), the host must PROVIDE it (`capabilities[k]
39
+ * === true`). `undefined`/`false` on the host means NOT satisfied — never silently
40
+ * widened (cf. the secure-floor posture). Refuses LOUDLY, naming the gaps; the
41
+ * full host capability-diff gate is deferred to M6.
42
+ */
43
+ export function checkHostCapabilities(requires, capabilities, hostName = "host") {
44
+ const unmet = [];
45
+ for (const k of BOOLEAN_CAPS) {
46
+ if (requires[k] === true && capabilities[k] !== true) {
47
+ unmet.push(`${k} (required true, host has ${JSON.stringify(capabilities[k])})`);
48
+ }
49
+ }
50
+ if (unmet.length > 0) {
51
+ throw new Error(`checkHostCapabilities: '${hostName}' cannot satisfy required capabilities (ADR-0008): ${unmet.join("; ")}`);
52
+ }
53
+ }
@@ -0,0 +1,24 @@
1
+ import type { CapabilityProfile } from "@irisrun/agent";
2
+ import type { HostAdapter } from "./adapter.js";
3
+ export interface CapabilityGap {
4
+ capability: keyof CapabilityProfile;
5
+ required: unknown;
6
+ hostProvides: unknown;
7
+ message: string;
8
+ }
9
+ /**
10
+ * Diff an image's required CapabilityProfile against a host's capabilities,
11
+ * returning the structured gaps (empty ⇒ deployable). Booleans reuse the
12
+ * checkHostCapabilities rule; tool_locality compares the image's DEMAND to the
13
+ * host's CEILING by the fixed rank remote<local<in-process. The local-tools-on-edge
14
+ * gap (local_subprocess required, OR tool_locality over ceiling) carries the
15
+ * literal ADR-0008 message; every other boolean gap carries the precise per-cap
16
+ * message. Never silently widens: undefined/false on the host = NOT satisfied.
17
+ */
18
+ export declare function diffCapabilities(requires: CapabilityProfile, host: HostAdapter): CapabilityGap[];
19
+ /**
20
+ * The deploy gate: throw an Error joining every gap's precise message if the
21
+ * image cannot run on the host; return silently if it can. Refuse LOUDLY — never
22
+ * degrade or silently widen the host's policy.
23
+ */
24
+ export declare function assertDeployable(requires: CapabilityProfile, host: HostAdapter): void;
@@ -0,0 +1,93 @@
1
+ // The boolean capability keys (tool_locality is the STRING dimension, handled
2
+ // separately by rank). Mirrors adapter.ts's BOOLEAN_CAPS, kept local so the two
3
+ // gates stay independent.
4
+ const BOOLEAN_CAPS = [
5
+ "long_running",
6
+ "local_subprocess",
7
+ "filesystem",
8
+ "websockets",
9
+ ];
10
+ // tool_locality demand rank: remote is the LEAST host-demanding (any host can
11
+ // reach a network endpoint; the only option on edge), in-process the MOST (the
12
+ // tool runs inside the core process — only a trusted same-language host). The
13
+ // host side reads its tool_locality as the CEILING; an image demanding a
14
+ // higher-ranked locality than the ceiling cannot be satisfied.
15
+ const LOCALITY_RANK = {
16
+ remote: 0,
17
+ local: 1,
18
+ "in-process": 2,
19
+ };
20
+ function localityRank(v) {
21
+ // An absent demand defaults to `remote` (the least-demanding, edge-safe floor).
22
+ return LOCALITY_RANK[v ?? "remote"];
23
+ }
24
+ /** The literal ADR-0008 refusal (agent-framework.md:682), interpolating the host
25
+ * label. With `name === "Cloudflare"` the result is byte-identical to the example. */
26
+ function adr0008Refusal(hostName) {
27
+ return `this agent requires local_subprocess tools; the ${hostName} target supports remote MCP tools only. Set tool_locality: remote or choose a VPS/serverless target.`;
28
+ }
29
+ /**
30
+ * Diff an image's required CapabilityProfile against a host's capabilities,
31
+ * returning the structured gaps (empty ⇒ deployable). Booleans reuse the
32
+ * checkHostCapabilities rule; tool_locality compares the image's DEMAND to the
33
+ * host's CEILING by the fixed rank remote<local<in-process. The local-tools-on-edge
34
+ * gap (local_subprocess required, OR tool_locality over ceiling) carries the
35
+ * literal ADR-0008 message; every other boolean gap carries the precise per-cap
36
+ * message. Never silently widens: undefined/false on the host = NOT satisfied.
37
+ */
38
+ export function diffCapabilities(requires, host) {
39
+ const caps = host.capabilities;
40
+ const gaps = [];
41
+ for (const k of BOOLEAN_CAPS) {
42
+ if (k === "local_subprocess")
43
+ continue; // handled with the locality dimension below
44
+ if (requires[k] === true && caps[k] !== true) {
45
+ gaps.push({
46
+ capability: k,
47
+ required: true,
48
+ hostProvides: caps[k],
49
+ message: `${k} (required true, host has ${JSON.stringify(caps[k])})`,
50
+ });
51
+ }
52
+ }
53
+ // The local-tools-on-edge dimension: local_subprocess demanded, OR the
54
+ // requested tool_locality is more host-demanding than the host's ceiling. Both
55
+ // surface as the literal ADR-0008 refusal.
56
+ const ceiling = caps.tool_locality;
57
+ const wantsLocalSubprocess = requires.local_subprocess === true && caps.local_subprocess !== true;
58
+ const localityOverCeiling = localityRank(requires.tool_locality) > localityRank(ceiling);
59
+ if (wantsLocalSubprocess) {
60
+ gaps.push({
61
+ capability: "local_subprocess",
62
+ required: true,
63
+ hostProvides: caps.local_subprocess,
64
+ message: adr0008Refusal(host.name),
65
+ });
66
+ }
67
+ if (localityOverCeiling) {
68
+ gaps.push({
69
+ capability: "tool_locality",
70
+ required: requires.tool_locality,
71
+ hostProvides: ceiling,
72
+ message: adr0008Refusal(host.name),
73
+ });
74
+ }
75
+ return gaps;
76
+ }
77
+ /**
78
+ * The deploy gate: throw an Error joining every gap's precise message if the
79
+ * image cannot run on the host; return silently if it can. Refuse LOUDLY — never
80
+ * degrade or silently widen the host's policy.
81
+ */
82
+ export function assertDeployable(requires, host) {
83
+ const gaps = diffCapabilities(requires, host);
84
+ if (gaps.length > 0) {
85
+ // De-duplicate identical messages before joining: an image demanding BOTH
86
+ // local_subprocess AND an over-ceiling tool_locality is ONE root cause (local
87
+ // tools on a remote-only host) and both gaps carry the same literal ADR-0008
88
+ // refusal — the user-facing message must render that sentence ONCE, not twice.
89
+ // (diffCapabilities still returns both structured gaps for programmatic inspection.)
90
+ const messages = [...new Set(gaps.map((g) => g.message))];
91
+ throw new Error(messages.join("; "));
92
+ }
93
+ }
@@ -0,0 +1,5 @@
1
+ export declare const PACKAGE = "@irisrun/host";
2
+ export { runTurnOn, checkHostCapabilities } from "./adapter.js";
3
+ export type { HostAdapter, RunTurnOnOptions } from "./adapter.js";
4
+ export { diffCapabilities, assertDeployable } from "./capabilities.js";
5
+ export type { CapabilityGap } from "./capabilities.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // @irisrun/host — public surface (host-side; makes "same image, different host" explicit).
2
+ export const PACKAGE = "@irisrun/host";
3
+ export { runTurnOn, checkHostCapabilities } from "./adapter.js";
4
+ export { diffCapabilities, assertDeployable } from "./capabilities.js";
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@irisrun/host",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Iris host adapter surface — HostAdapter {name, capabilities, store, scheduler}, runTurnOn (run a turn ON a host's store+scheduler), and checkHostCapabilities (the tool/host-level ADR-0008 refusal). Makes 'same image, different host' explicit for the portability proof. Host-side; zero external deps.",
6
+ "exports": {
7
+ ".": {
8
+ "iris-src": "./src/index.ts",
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "dependencies": {
14
+ "@irisrun/core": "^0.1.0",
15
+ "@irisrun/agent": "^0.1.0"
16
+ },
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">=24"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/xoai/iris.git",
27
+ "directory": "packages/host"
28
+ },
29
+ "homepage": "https://github.com/xoai/iris#readme",
30
+ "files": [
31
+ "dist"
32
+ ]
33
+ }