operon-agents 0.1.0-alpha.16 → 0.1.0-alpha.18

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.
@@ -204,7 +204,7 @@ export interface SessionInvokeResult {
204
204
  * This list stays explicit, one entry per reachable operation: it is a security boundary, so a
205
205
  * new capability operation should have to be named here to become remotely callable.
206
206
  */
207
- export declare const INVOKABLE_METHODS: readonly ["setModel", "setThinking", "setPermissionMode", "goal.create", "goal.snapshot", "goal.pause", "goal.resume", "goal.cancel", "goal.changeBudget", "plan.setEnabled", "plan.data", "compaction.request", "compaction.pending", "compaction.cancel", "skills.listSkills", "skills.activateSkill", "plugins.summaries", "plugins.info", "plugins.installSummary", "plugins.setEnabled", "plugins.setMcpServerEnabled", "plugins.remove", "plugins.reload", "background.list", "background.readOutput", "background.readOutputDelta", "background.stop", "background.detach", "background.listSubagents", "background.reconcileSubagents", "background.listWorkflows", "background.getWorkflow", "runCommand", "getRecords", "getContextBreakdown", "mcp.list", "mcp.listTools", "mcp.reconnect"];
207
+ export declare const INVOKABLE_METHODS: readonly ["setModel", "setThinking", "setPermissionMode", "goal.create", "goal.snapshot", "goal.pause", "goal.resume", "goal.cancel", "goal.changeBudget", "plan.setEnabled", "plan.data", "compaction.request", "compaction.pending", "compaction.cancel", "skills.listSkills", "skills.activateSkill", "plugins.summaries", "plugins.info", "plugins.installSummary", "plugins.setEnabled", "plugins.setMcpServerEnabled", "plugins.remove", "plugins.reload", "background.list", "background.readOutput", "background.readOutputDelta", "background.stop", "background.detach", "background.listSubagents", "background.reconcileSubagents", "background.listWorkflows", "background.getWorkflow", "runCommand", "getRecords", "getAuditRecords", "getContextBreakdown", "mcp.list", "mcp.listTools", "mcp.reconnect"];
208
208
  export type InvokableMethod = (typeof INVOKABLE_METHODS)[number];
209
209
  /** `session/event` — an AgentEvent verbatim (already carries `sessionId` + `address`). */
210
210
  export type SessionEventParams = AgentEvent;
@@ -95,8 +95,9 @@ export const INVOKABLE_METHODS = [
95
95
  // slash commands — the serializable control surface for extensions (cron et al.):
96
96
  // per-feature cron RPCs are gone with the capability, /cron rides this instead.
97
97
  "runCommand",
98
- // conversation log
98
+ // conversation log, and the audit shard beside it
99
99
  "getRecords",
100
+ "getAuditRecords",
100
101
  // context introspection
101
102
  "getContextBreakdown",
102
103
  // mcp
@@ -83,8 +83,8 @@ export declare class ExtensionRuntime {
83
83
  /** Runs one extension's `session` and files its scope. Failure ⇒ skipped, contributions undone. */
84
84
  private setupExtension;
85
85
  /**
86
- * Durable audit trail: replay needs "tool X only exists after record N" to reconstruct the
87
- * capability timeline. Rides the `custom` record type — audit-only, ignored by reducers.
86
+ * Durable audit trail of when this extension joined or left the session — written to the
87
+ * session's audit shard, which nothing replays from.
88
88
  */
89
89
  private logChange;
90
90
  listTools(): readonly Tool[];
@@ -1,5 +1,5 @@
1
1
  import { ServiceUnavailableError, isProbeProperty } from "./services.js";
2
- import { tagToolSource } from "operon-agents-core";
2
+ import { appendAudit, tagToolSource } from "operon-agents-core";
3
3
  /** Decision points block the loop, so they get room to do real work (network, subprocess). */
4
4
  const DECISION_TIMEOUT_MS = 30_000;
5
5
  /** Observers must not become latency: a slow one is dropped, not waited on. */
@@ -162,8 +162,8 @@ export class ExtensionRuntime {
162
162
  }
163
163
  }
164
164
  /**
165
- * Durable audit trail: replay needs "tool X only exists after record N" to reconstruct the
166
- * capability timeline. Rides the `custom` record type — audit-only, ignored by reducers.
165
+ * Durable audit trail of when this extension joined or left the session — written to the
166
+ * session's audit shard, which nothing replays from.
167
167
  */
168
168
  async logChange(kind) {
169
169
  const extensionId = this.id;
@@ -171,7 +171,7 @@ export class ExtensionRuntime {
171
171
  if (!store)
172
172
  return;
173
173
  try {
174
- await store.appendRecord({ type: "custom", name: `extensions.${kind}`, data: { extensionId } });
174
+ await appendAudit(store, { type: "custom", name: `extensions.${kind}`, data: { extensionId } });
175
175
  }
176
176
  catch (error) {
177
177
  await this.warn(extensionId, `failed to journal extension ${kind}: ${messageOf(error)}`);
@@ -697,7 +697,7 @@ export class ExtensionRuntime {
697
697
  return this.revocable(extensionId, {
698
698
  steer: (content, options) => runtime.enqueue(extensionId, content, "steering", options?.metadata),
699
699
  followUp: (content, options) => runtime.enqueue(extensionId, content, "follow_up", options?.metadata),
700
- // Journaled by the engine into the main conversation as `extension:<id>:<name>`, and seen
700
+ // Written by the engine to the session's audit shard as `extension:<id>:<name>`, and seen
701
701
  // by `records()` straight away.
702
702
  record: (name, data) => {
703
703
  const session = runtime.session;
package/dist/harness.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * low-level "assemble it yourself / stateless server" path, import from
12
12
  * `operon-agents/core` instead.
13
13
  */
14
- import { type AgentEvent, type EventPublicationMode, type TelemetryService, type AgentEventListener, type AgentInput, SessionProjection, type SessionSnapshot, type SnapshotOptions, type DirectoryEntry, type ProjectionObservation, type ProjectionListener, type ApprovalRequest, type ApprovalResponse, type Capability, type InterruptAnswer, type ChatModel, ListenerSink, type PermissionManagerOptions, type PermissionMode, type PendingRunInterrupt, type QuestionRequest, type QuestionResult, type Responder, type RunHandle, type RunResult, type SessionStore, type SessionHandle, type SessionSummary, type ListSessionsFilter, type OpenSessionOptions, type DeleteSessionOptions, type ThinkingLevel, type Tool, type SteerMessage, type SteerOptions, type SteerOrigin, type SteerReceipt, Agent, Runner, Session, McpOAuthService, SkillRegistry, type HookDef, type McpServerConfig, type McpTransportKind, type PluginManager, type ApprovalRequestOptions, type Environment, type EnvironmentFactory, type SessionRepository, type ResolvedAgentProfile, type SubagentProvider } from "operon-agents-core";
14
+ import { type AgentEvent, type EventPublicationMode, type TelemetryService, type AgentEventListener, type AgentInput, SessionProjection, type SessionSnapshot, type SnapshotOptions, type DirectoryEntry, type ProjectionObservation, type ProjectionListener, type ApprovalRequest, type ApprovalResponse, type Capability, type InterruptAnswer, type ChatModel, ListenerSink, type PermissionManagerOptions, type PermissionMode, type PendingRunInterrupt, type QuestionRequest, type QuestionResult, type Responder, type RunHandle, type RunResult, type SessionStore, type SessionHandle, type SessionSummary, type ListSessionsFilter, type OpenSessionOptions, type DeleteSessionOptions, type ThinkingLevel, type Tool, type SteerMessage, type SteerOptions, type SteerOrigin, type SteerReceipt, Agent, Runner, Session, McpOAuthService, SkillRegistry, type HookDef, type McpServerConfig, type McpTransportKind, type McpCredentialProvider, type PluginManager, type ApprovalRequestOptions, type Environment, type EnvironmentFactory, type SessionRepository, type ResolvedAgentProfile, type SubagentProvider } from "operon-agents-core";
15
15
  import type { SkillsService, CompactionService, BackgroundManager, McpServersHandle, GoalStore, PlanMode, Logger, ModelRuntime, TracingProcessor } from "operon-agents-core";
16
16
  import { type WorkspaceMcpPool } from "./workspace-mcp.ts";
17
17
  import { HarnessExtensionManager, ServiceRegistry, type ExtensionDefinition, type ServiceOptions } from "./extensions/index.ts";
@@ -107,6 +107,17 @@ export interface DefaultCapabilitiesOptions {
107
107
  * to run is the host's statement, not the kernel's.
108
108
  */
109
109
  readonly allowedMcpTransports?: readonly McpTransportKind[];
110
+ /** Credentials the host hands out per connect for this session's http MCP servers. See
111
+ * `McpCredentialProvider`. */
112
+ readonly mcpCredentials?: McpCredentialProvider;
113
+ /**
114
+ * The session belongs to one tenant among many (a managed host's default): nothing the process
115
+ * shares reaches it — no process-wide MCP set, skill scan, OAuth service or directory pool —
116
+ * MCP runs over http only, and no server may read its token from the process environment.
117
+ * Each is refused rather than skipped where skipping would hide a misconfiguration. See
118
+ * docs/server-profile.md.
119
+ */
120
+ readonly strictIsolation?: boolean;
110
121
  }
111
122
  /**
112
123
  * The standard coding-agent capability bundle (goal / plan / todo / skills /
@@ -300,6 +311,11 @@ export interface SessionCapabilityContext {
300
311
  /** The session brought its own environment (`{ environment }` on this open): harness-shared objects
301
312
  * derived from the harness's environment (the skill scan) do not apply to it. */
302
313
  readonly ownEnvironment: boolean;
314
+ /** {@link OpenSessionOptionsBase.mcpCredentials} for this open. */
315
+ readonly mcpCredentials?: McpCredentialProvider;
316
+ /** {@link OpenSessionOptionsBase.strictIsolation} for this open. A custom factory owns honouring
317
+ * it the way `defaultCapabilities` does. */
318
+ readonly strictIsolation: boolean;
303
319
  }
304
320
  /**
305
321
  * Options common to every way of opening a session.
@@ -327,6 +343,16 @@ interface OpenSessionOptionsBase<TContext = unknown> {
327
343
  * shared one for this session; a custom factory can decide otherwise.
328
344
  */
329
345
  readonly mcpServers?: Record<string, McpServerConfig>;
346
+ /**
347
+ * Where this session's http MCP servers get their credentials, asked on every connect: the
348
+ * seam a host builds a per-user vault on. Not persisted — a resume passes it again.
349
+ */
350
+ readonly mcpCredentials?: McpCredentialProvider;
351
+ /**
352
+ * This session is one tenant's among many: see `DefaultCapabilitiesOptions.strictIsolation`.
353
+ * A managed host's worker sets it on every open unless it runs single-tenant.
354
+ */
355
+ readonly strictIsolation?: boolean;
330
356
  /**
331
357
  * Per-session argument for each registered extension, by extension id — handed to that
332
358
  * extension's `session(api, ctx)` as `ctx.params`. A value of `false` skips the extension for
@@ -601,6 +627,7 @@ export declare class HarnessSession<TContext = unknown> {
601
627
  /** PROBE tier: absent without an MCP capability. */
602
628
  get mcp(): McpServersHandle | undefined;
603
629
  getRecords(...args: Parameters<Session["getRecords"]>): ReturnType<Session["getRecords"]>;
630
+ getAuditRecords(): ReturnType<Session["getAuditRecords"]>;
604
631
  getContextBreakdown(): ReturnType<Session["getContextBreakdown"]>;
605
632
  /** Where this session's tools run. A host uses it to run a command in the same place the agent
606
633
  * does — one shell, one filesystem, one working directory. */
@@ -689,6 +716,12 @@ export declare class Harness<TContext = unknown> {
689
716
  /** The extension whose `harness` is running right now — its `createSession` must refuse. */
690
717
  private runningCreate;
691
718
  constructor(options: HarnessOptions<TContext>);
719
+ /**
720
+ * Which session-facing parts the `harness` hook built for the whole process — what a
721
+ * multi-tenant host must not have, since every session on the process would share them.
722
+ * Infrastructure (repository, logger, model runtime) is not listed: sharing it is the point.
723
+ */
724
+ processSharedParts(): Promise<readonly ("mcpServers" | "oauthService" | "skillRegistry")[]>;
692
725
  /** The session repository, once the `harness` hook has had its say. */
693
726
  private repository;
694
727
  /** The fallback repository, built once so two opens share one in-memory store. */
package/dist/harness.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * low-level "assemble it yourself / stateless server" path, import from
12
12
  * `operon-agents/core` instead.
13
13
  */
14
- import { readLog, SessionProjection, ListenerSink, agentEventFromRecord, newAgentEventId, Agent, buildAgentFromProfile, INTERRUPTION_STATE_KEY, flattenPendingInterrupts, parseInterruptionState, Runner, Session, emptyUsage, isGuardrailTripwireError, askUserQuestionTool, backgroundCapability, compactionCapability, filesystemTools, goalCapability, LocalEnvironment, McpOAuthService, MemorySessionRepository, SessionRepositoryNotFoundError, planCapability, skillsCapability, SkillRegistry, mcpServersCapability, pluginsCapability, userHooksCapability, taskCapability, workflowCapability, DEFAULT_AGENT_PROFILES, DEFAULT_AGENT_PROFILE_NAME, DEFAULT_SUBAGENT_NAMES, profileSubagentProvider, DEFAULT_ADDRESS, envLogger, noopLogger, } from "operon-agents-core";
14
+ import { readSessionTails, SessionProjection, ListenerSink, agentEventFromRecord, newAgentEventId, Agent, buildAgentFromProfile, INTERRUPTION_STATE_KEY, flattenPendingInterrupts, parseInterruptionState, Runner, Session, emptyUsage, isGuardrailTripwireError, askUserQuestionTool, backgroundCapability, compactionCapability, filesystemTools, goalCapability, LocalEnvironment, McpOAuthService, MemorySessionRepository, SessionRepositoryNotFoundError, planCapability, skillsCapability, SkillRegistry, mcpServersCapability, pluginsCapability, userHooksCapability, taskCapability, workflowCapability, DEFAULT_AGENT_PROFILES, DEFAULT_AGENT_PROFILE_NAME, DEFAULT_SUBAGENT_NAMES, profileSubagentProvider, DEFAULT_ADDRESS, envLogger, noopLogger, } from "operon-agents-core";
15
15
  import { mkdirSync } from "node:fs";
16
16
  import { workspaceMcpCapability } from "./workspace-mcp.js";
17
17
  import { join } from "node:path";
@@ -42,9 +42,15 @@ const EXTENSION_PARAMS_STATE_KEY = "extensions:params";
42
42
  export function defaultCapabilities(options = {}) {
43
43
  const manager = options.pluginManager ?? options.shared?.pluginManager;
44
44
  // A session on its own environment ignores the process-wide registry: it was scanned elsewhere.
45
- const sharedSkills = options.ownEnvironment === true ? undefined : options.shared?.skillRegistry;
46
- const sharedServers = options.ownEnvironment === true ? undefined : options.shared?.mcpServers;
47
- const oauthService = options.oauthService ?? options.shared?.oauthService;
45
+ const strict = options.strictIsolation === true;
46
+ // A session on its own environment, or one tenant among many, gets nothing the process shares.
47
+ const isolated = strict || options.ownEnvironment === true;
48
+ const sharedSkills = isolated ? undefined : options.shared?.skillRegistry;
49
+ const sharedServers = isolated ? undefined : options.shared?.mcpServers;
50
+ const oauthService = options.oauthService ?? (strict ? undefined : options.shared?.oauthService);
51
+ const allowedMcpTransports = strict
52
+ ? (options.allowedMcpTransports ?? ["http", "stdio"]).filter((kind) => kind === "http")
53
+ : options.allowedMcpTransports;
48
54
  // Shared registry so the plugin session-start injector can render a skill the skills capability
49
55
  // loaded (its roots include the plugin skill dirs below). A process-wide registry is scanned
50
56
  // once for every session; a session-level one is scanned here.
@@ -69,7 +75,8 @@ export function defaultCapabilities(options = {}) {
69
75
  const sessionMcp = options.sessionMcpServers ?? {};
70
76
  const mcpOptions = {
71
77
  ...(oauthService !== undefined ? { oauthService } : {}),
72
- ...(options.allowedMcpTransports !== undefined ? { allowedTransports: options.allowedMcpTransports } : {}),
78
+ ...(allowedMcpTransports !== undefined ? { allowedTransports: allowedMcpTransports } : {}),
79
+ ...(options.mcpCredentials !== undefined ? { credentials: options.mcpCredentials } : {}),
73
80
  };
74
81
  // Everything this session would connect that is not private to it: the directory's servers
75
82
  // last, so they win over a same-named plugin server the way they did as a session overlay.
@@ -78,7 +85,15 @@ export function defaultCapabilities(options = {}) {
78
85
  ...(manager?.mcpServerConfigs() ?? {}),
79
86
  ...(options.workspaceMcp?.servers ?? {}),
80
87
  };
81
- const pooled = sharedServers === undefined && options.ownEnvironment !== true ? options.workspaceMcp : undefined;
88
+ if (strict) {
89
+ // The process environment has no tenant boundary: a token read from it is every tenant's.
90
+ for (const [name, config] of Object.entries({ ...shareable, ...sessionMcp })) {
91
+ if (config.bearerTokenEnvVar !== undefined) {
92
+ throw new Error(`MCP server "${name}" reads its token from the process environment (bearerTokenEnvVar), which every tenant on this host shares; hand it out per session with mcpCredentials instead.`);
93
+ }
94
+ }
95
+ }
96
+ const pooled = sharedServers === undefined && !isolated ? options.workspaceMcp : undefined;
82
97
  if (pooled !== undefined && Object.keys(shareable).length > 0) {
83
98
  // Borrowed per directory: the capability views the pool's set, `sessionMcp` is its overlay.
84
99
  capabilities.push(workspaceMcpCapability({ pool: pooled.pool, workDir: pooled.workDir, servers: shareable, overlay: sessionMcp, overlayOptions: mcpOptions }));
@@ -698,6 +713,9 @@ export class HarnessSession {
698
713
  getRecords(...args) {
699
714
  return this.core.getRecords(...args);
700
715
  }
716
+ getAuditRecords() {
717
+ return this.core.getAuditRecords();
718
+ }
701
719
  // ── context introspection — where the model's window is spent, as of the last turn ──
702
720
  getContextBreakdown() {
703
721
  return this.core.getContextBreakdown();
@@ -920,6 +938,22 @@ export class Harness {
920
938
  // Surfaced to whoever opens a session; never an unhandled rejection on its own.
921
939
  this.sharedReady.catch(() => undefined);
922
940
  }
941
+ /**
942
+ * Which session-facing parts the `harness` hook built for the whole process — what a
943
+ * multi-tenant host must not have, since every session on the process would share them.
944
+ * Infrastructure (repository, logger, model runtime) is not listed: sharing it is the point.
945
+ */
946
+ async processSharedParts() {
947
+ await this.sharedReady;
948
+ const parts = [];
949
+ if (this.parts.mcpServers !== undefined)
950
+ parts.push("mcpServers");
951
+ if (this.parts.oauthService !== undefined)
952
+ parts.push("oauthService");
953
+ if (this.parts.skillRegistry !== undefined)
954
+ parts.push("skillRegistry");
955
+ return parts;
956
+ }
923
957
  /** The session repository, once the `harness` hook has had its say. */
924
958
  async repository() {
925
959
  await this.sharedReady;
@@ -1379,7 +1413,17 @@ export class Harness {
1379
1413
  }
1380
1414
  /** Resolve the capability set for one session — the `session` hook, called fresh every time. */
1381
1415
  async resolveCapabilities(ctx, params = {}) {
1382
- const base = this.options.session !== undefined ? await this.options.session(ctx) : defaultCapabilities({ shared: this.parts });
1416
+ // A session on its own environment (a sandbox) does not get the process's MCP connections or
1417
+ // skill scan: both describe the HARNESS's machine, not the one this session's tools run on.
1418
+ const base = this.options.session !== undefined
1419
+ ? await this.options.session(ctx)
1420
+ : defaultCapabilities({
1421
+ shared: this.parts,
1422
+ ownEnvironment: ctx.ownEnvironment,
1423
+ strictIsolation: ctx.strictIsolation,
1424
+ ...(ctx.mcpServers !== undefined ? { sessionMcpServers: ctx.mcpServers } : {}),
1425
+ ...(ctx.mcpCredentials !== undefined ? { mcpCredentials: ctx.mcpCredentials } : {}),
1426
+ });
1383
1427
  // Every definition registered on this harness: by value (`extensions`, harness halves
1384
1428
  // included — their per-session `session` is mounted here like any other), then from files.
1385
1429
  const extensions = [...this.valueDefs.values(), ...(this.extensions?.sessionDefinitions() ?? [])];
@@ -1459,12 +1503,13 @@ export class Harness {
1459
1503
  // Environment: this call's override → the harness's default → a LocalEnvironment at the
1460
1504
  // session's own workDir. The session only OPERATES a caller-supplied environment.
1461
1505
  const environment = opts.environment ?? this.parts.environment ?? new LocalEnvironment(workDir);
1462
- // ONE open-time log read, shared: the projection seeds from it, and Session.open
1463
- // receives it as `preloadedLog` so its capability restore + context pre-build fold the
1464
- // same records — same IO, and the same `Message` objects (no second parsed copy).
1506
+ // ONE open-time log read, shared: every shard from its newest checkpoint on (each
1507
+ // checkpoint carries the history and lifecycle before it). The projection seeds from it,
1508
+ // and Session.open receives it as `preloadedLog` so its capability restore + context
1509
+ // pre-build fold the same records — same IO, and the same `Message` objects.
1465
1510
  // Attach still happens BEFORE Session.open: no events flow yet, so the seed cannot
1466
1511
  // race the subscription, and anything capabilities emit during open is already folded.
1467
- let preloadedLog = await readLog(store);
1512
+ let preloadedLog = await readSessionTails(store);
1468
1513
  // A fresh session has an empty log; anything else is a reopen. Telemetry is the only reader.
1469
1514
  const resumed = preloadedLog.length > 0;
1470
1515
  projection = await SessionProjection.attach({ id, store, events }, preloadedLog);
@@ -1478,7 +1523,9 @@ export class Harness {
1478
1523
  sessionId: id,
1479
1524
  workDir,
1480
1525
  ownEnvironment: opts.environment !== undefined,
1526
+ strictIsolation: opts.strictIsolation === true,
1481
1527
  ...(opts.mcpServers !== undefined ? { mcpServers: opts.mcpServers } : {}),
1528
+ ...(opts.mcpCredentials !== undefined ? { mcpCredentials: opts.mcpCredentials } : {}),
1482
1529
  }, params);
1483
1530
  // Everything the session runs on, by value. The repository owns the store's lifetime.
1484
1531
  core = await Session.open({
@@ -1595,7 +1642,7 @@ export class Harness {
1595
1642
  export async function closeOrphanedTurns(sessionId, store) {
1596
1643
  if ((await store.getState(INTERRUPTION_STATE_KEY)) !== null)
1597
1644
  return [];
1598
- const projection = await SessionProjection.attach({ id: sessionId, store, events: new ListenerSink() }, await readLog(store));
1645
+ const projection = await SessionProjection.attach({ id: sessionId, store, events: new ListenerSink() }, await readSessionTails(store));
1599
1646
  const records = await closeOrphanedProjectionFrames(sessionId, store, projection);
1600
1647
  const events = [];
1601
1648
  for (const record of records) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "operon-agents",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.18",
4
4
  "description": "Public SDK entrypoint for building agents.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -73,11 +73,11 @@
73
73
  "posthog-node": "^5.51.6",
74
74
  "zod": "^4.4.3",
75
75
  "smol-toml": "^1.6.1",
76
- "@earendil-works/pi-ai": "0.87.1",
77
- "operon-agents-core": "0.1.0-alpha.16"
76
+ "@earendil-works/pi-ai": "1.3.0",
77
+ "operon-agents-core": "0.1.0-alpha.18"
78
78
  },
79
79
  "devDependencies": {
80
- "@earendil-works/pi-ai": "0.87.1"
80
+ "@earendil-works/pi-ai": "1.3.0"
81
81
  },
82
82
  "scripts": {
83
83
  "clean": "rm -rf dist *.tsbuildinfo",