Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,25 @@ export {
export { type RawGatewayEnvelope } from './shim.js';
export { envelopeToAgentEvent } from './to-agent-event.js';

// Versioned Agent compiler and Run engine contracts. These are additive to
// the existing simulation record while hosted/local producers migrate.
export {
LOCAL_EFFECT_POLICY_DEFAULTS,
resolveLocalEffectPolicy,
type CompiledAgentV1,
type Diagnostic,
type EffectPolicyV1,
type PreviewAction,
type RunArtifactEntry,
type RunArtifactManifest,
type RunMode,
type RunRecordV2,
type RunRequestV1,
type RunTraceEventV1,
type StateDiff,
type StateSourceV1
} from './run-contracts.js';

export type {
LinearAgentActivity,
LinearAgentActivityType,
Expand Down
109 changes: 109 additions & 0 deletions packages/runtime/src/run-contracts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
LOCAL_EFFECT_POLICY_DEFAULTS,
resolveLocalEffectPolicy,
type PreviewAction,
type RunRecordV2
} from './run-contracts.js';

test('local EffectPolicyV1 defaults are fixture/stub/preview safe', () => {
assert.deepEqual(LOCAL_EFFECT_POLICY_DEFAULTS, {
reads: 'fixtures',
writes: 'preview',
model: 'stub',
shell: 'simulate',
compose: 'preview',
allowedHttp: []
});
assert.throws(
() => LOCAL_EFFECT_POLICY_DEFAULTS.allowedHttp.push({ method: 'GET', urlGlob: '*' }),
TypeError
);
});

test('local policy resolver copies HTTP defaults instead of sharing mutable state', () => {
const first = resolveLocalEffectPolicy();
const second = resolveLocalEffectPolicy();
assert.notEqual(first.allowedHttp, LOCAL_EFFECT_POLICY_DEFAULTS.allowedHttp);
assert.notEqual(first.allowedHttp, second.allowedHttp);
});

test('local effect policy cannot be escalated to live writes, shell, or compose', () => {
assert.deepEqual(resolveLocalEffectPolicy({ writes: 'live', shell: 'live', compose: 'live' }), {
reads: 'fixtures',
writes: 'preview',
model: 'stub',
shell: 'simulate',
compose: 'preview',
allowedHttp: []
});
assert.equal(resolveLocalEffectPolicy({ writes: 'deny' }).writes, 'deny');
});

test('RunRecordV2 retains additive existing and extension fields', () => {
const record: RunRecordV2 = {
runId: 'run_1',
status: 'succeeded',
origin: 'local_dry_run',
mode: 'preview',
policy: resolveLocalEffectPolicy(),
eventId: 'evt_1',
eventContract: 'cron.tick@1',
trace: [],
actions: [],
artifacts: { artifacts: [] },
stateDiff: {},
legacyCloudField: { retained: true }
};
assert.deepEqual(record.legacyCloudField, { retained: true });
});

test('RunRecordV2 JSON round-trip preserves richer transport preview fields', () => {
interface TransportPreviewAction extends PreviewAction {
method: 'write';
path: string;
/** Mirrors the narrower WS-D transport query-parameter contract. */
parameters: Record<string, string | number>;
body: Record<string, unknown>;
simulatedReceipt: { id: string; timestamp: string };
}

const transportAction: TransportPreviewAction = {
kind: 'provider.write',
status: 'previewed',
provider: 'slack',
resource: 'messages',
method: 'write',
path: '/slack/channels/C123/messages/drafts/draft-1.json',
parameters: { channel: 'C123' },
body: { text: 'Preview only' },
simulatedReceipt: { id: 'sim_1', timestamp: '2026-07-15T09:00:00.000Z' },
data: {
operation: 'write',
simulatedReceipt: { id: 'sim_1', timestamp: '2026-07-15T09:00:00.000Z' }
}
};
const record: RunRecordV2 = {
runId: 'run_transport',
status: 'succeeded',
origin: 'local_dry_run',
mode: 'preview',
policy: resolveLocalEffectPolicy(),
eventId: 'evt_transport',
eventContract: 'slack.message.created@1',
trace: [],
actions: [transportAction],
artifacts: { artifacts: [] },
stateDiff: {}
};

const roundTripped = JSON.parse(JSON.stringify(record)) as RunRecordV2;
const action = roundTripped.actions[0] as PreviewAction & Partial<TransportPreviewAction>;
assert.equal(action.method, 'write');
assert.equal(action.path, '/slack/channels/C123/messages/drafts/draft-1.json');
assert.deepEqual(action.simulatedReceipt, {
id: 'sim_1',
timestamp: '2026-07-15T09:00:00.000Z'
});
});
147 changes: 147 additions & 0 deletions packages/runtime/src/run-contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { EventFrameV1 } from '@agentworkforce/events';
import type { AgentSpec, PersonaSpec } from '@agentworkforce/persona-kit';

export interface Diagnostic {
severity: 'warning' | 'error';
code: string;
message: string;
path?: string;
extensions?: Record<string, unknown>;
}

/** Stable compiler output shared by deploy, invoke, and Run construction. */
export interface CompiledAgentV1 {
schemaVersion: 1;
sourceKind: 'single-file' | 'split';
sourcePath: string;
persona: PersonaSpec;
agent: AgentSpec;
handlerEntry: string;
sourceDigest: string;
compileWarnings: Diagnostic[];
extensions?: Record<string, unknown>;
}

export type RunMode = 'simulate' | 'preview' | 'sandbox' | 'hosted' | 'replay';

export interface EffectPolicyV1 {
reads: 'deny' | 'fixtures' | 'live';
writes: 'deny' | 'preview' | 'sandbox' | 'live';
model: 'stub' | 'fixture' | 'live';
shell: 'deny' | 'simulate' | 'sandbox' | 'live';
compose: 'deny' | 'preview' | 'sandbox' | 'live';
allowedHttp: Array<{ method: string; urlGlob: string }>;
allowedProviders?: string[];
}

/** Safe policy floor used by local invoke before case-specific narrowing. */
const EMPTY_ALLOWED_HTTP = Object.freeze([]) as unknown as EffectPolicyV1['allowedHttp'];

export const LOCAL_EFFECT_POLICY_DEFAULTS: Readonly<EffectPolicyV1> = Object.freeze({
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
reads: 'fixtures',
writes: 'preview',
model: 'stub',
shell: 'simulate',
compose: 'preview',
allowedHttp: EMPTY_ALLOWED_HTTP
});
Comment thread
miyaontherelay marked this conversation as resolved.

export interface StateSourceV1 {
schemaVersion: 1;
kind: 'empty' | 'fixtures' | 'workspace' | 'replay';
ref?: string;
fidelity?: 'historical' | 'current' | 'fixture' | 'simulated' | 'unavailable';
extensions?: Record<string, unknown>;
}

export interface RunRequestV1 {
schemaVersion: 1;
agent: CompiledAgentV1;
event: EventFrameV1;
mode: RunMode;
inputs: Record<string, string>;
policy: EffectPolicyV1;
state: StateSourceV1;
clock?: { now: string };
parentRunId?: string;
composeId?: string;
}

export interface RunTraceEventV1 {
schemaVersion: 1;
seq: number;
at: string;
runId: string;
parentSpanId?: string;
spanId: string;
kind: string;
phase: 'route' | 'read' | 'decide' | 'model' | 'write' | 'compose' | 'result';
status: 'started' | 'succeeded' | 'failed' | 'denied' | 'previewed';
summary: string;
data?: Record<string, unknown>;
artifactRefs?: string[];
}

export interface PreviewAction {
id?: string;
kind: string;
provider?: string;
resource?: string;
status?: 'denied' | 'previewed' | 'sandboxed' | 'executed';
data?: Record<string, unknown>;
extensions?: Record<string, unknown>;
}

export interface RunArtifactEntry {
id: string;
kind: string;
path?: string;
mediaType?: string;
redacted: boolean;
extensions?: Record<string, unknown>;
}

export interface RunArtifactManifest {
artifacts: RunArtifactEntry[];
extensions?: Record<string, unknown>;
}

export interface StateDiff {
files?: Array<{ path: string; before?: string; after?: string }>;
memory?: Array<Record<string, unknown>>;
providers?: Array<Record<string, unknown>>;
extensions?: Record<string, unknown>;
}

/** Additive common record; hosted/local implementations may retain existing fields. */
export interface RunRecordV2 {
runId: string;
status: 'succeeded' | 'failed' | 'cancelled';
origin: 'local_dry_run' | 'hosted';
mode: RunRequestV1['mode'];
policy: EffectPolicyV1;
eventId: string;
eventContract: string;
trace: RunTraceEventV1[];
actions: PreviewAction[];
artifacts: RunArtifactManifest;
stateDiff: StateDiff;
/** Existing Run fields remain serializable while producers migrate additively. */
[field: string]: unknown;
}

/** Apply local defaults while preventing callers from escalating local effects. */
export function resolveLocalEffectPolicy(
requested: Partial<EffectPolicyV1> = {}
): EffectPolicyV1 {
return {
reads: requested.reads ?? LOCAL_EFFECT_POLICY_DEFAULTS.reads,
writes: requested.writes === 'deny' ? 'deny' : 'preview',
model: requested.model ?? LOCAL_EFFECT_POLICY_DEFAULTS.model,
shell: requested.shell === 'deny' ? 'deny' : 'simulate',
compose: requested.compose === 'deny' ? 'deny' : 'preview',
allowedHttp: (requested.allowedHttp ?? LOCAL_EFFECT_POLICY_DEFAULTS.allowedHttp)
.map((rule) => ({ ...rule })),
...(requested.allowedProviders ? { allowedProviders: [...requested.allowedProviders] } : {})
};
}
Loading