@agentproto/runtime 0.5.0 → 0.6.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,256 @@
1
+ import { CatalogProvider } from '@agentproto/model-catalog';
2
+
3
+ /**
4
+ * Pure resolver for `~/.agentproto/config.json`'s `defaults` block —
5
+ * computes the effective `skills` + `options` + `auth` for an `agent_start`
6
+ * spawn before adapter-specific normalization. No fs, no adapter I/O, so
7
+ * it's unit-testable in isolation from `session-spawn.ts` (which owns the
8
+ * fs read + the adapter-manifest lookup).
9
+ *
10
+ * Precedence (lowest → highest): global `defaults` < `defaults.adapters.
11
+ * <slug>` < the explicit `agent_start` call.
12
+ * - `options`: shallow-merged maps, later (higher-precedence) keys win.
13
+ * - `skills`: global ∪ per-adapter when the caller didn't pass `skills`
14
+ * at all; an explicit `skills` (even `[]`) REPLACES the union rather
15
+ * than merging into it — a deliberate exact set, mirroring how an
16
+ * explicit `mcpServers: []` opts out of the hermes default in
17
+ * `session-spawn.ts`.
18
+ * - `auth`: surfaces the RAW billing-auth material (requested mode, both
19
+ * candidate credentials, per-spawn provider pin, and the `explicit`
20
+ * signal) so the descriptor-aware resolver ({@link resolveAuthSpec},
21
+ * which also needs the adapter's provider/subscription descriptor +
22
+ * providers.json) can decide the final mode, env var, scrub set, and
23
+ * credential source. Credentials are named in config or the provider
24
+ * store — never read from the ambient shell env.
25
+ */
26
+
27
+ /**
28
+ * Deterministic billing-auth config for one adapter slug (today, only
29
+ * claude-code interprets it — see `AgentCliStartOptions.auth` in
30
+ * `@agentproto/driver-agent-cli`). EXPLICIT credential selection, not
31
+ * scrub-by-absence: `token`/`apiKey` are the actual secret values, named
32
+ * here (or supplied per-spawn) rather than inherited from the launching
33
+ * shell. Never logged; only a fingerprint (see {@link credentialFingerprint})
34
+ * is ever surfaced back to a caller.
35
+ */
36
+ interface DefaultsAdapterAuthConfig {
37
+ /** `"subscription"` or `"api-key"`. Omitted ⇒ the resolver picks by
38
+ * ordered preference (subscription first for adapters that support it —
39
+ * see {@link resolveAuthSpec}), never a hardcoded default. */
40
+ mode?: "subscription" | "api-key";
41
+ /** The subscription bearer token for `"subscription"` mode — minted via
42
+ * `claude setup-token` (bills the Max/Pro subscription, not API credits),
43
+ * SET to the adapter's `authSubscription.setEnv`. */
44
+ token?: string;
45
+ /** Explicit API key for `"api-key"` mode, SET to `providerEnvVar(provider)`.
46
+ * Wins over the `providers.json` store key for the same provider. */
47
+ apiKey?: string;
48
+ /** Per-spawn provider PIN — overrides the adapter's fixed provider and the
49
+ * model-derived provider (the sharp edge for by-model routers whose config
50
+ * routes a catalog-"anthropic" model elsewhere). A `CatalogProvider` id. */
51
+ provider?: CatalogProvider;
52
+ }
53
+ interface DefaultsAdapterConfig {
54
+ skills?: string[];
55
+ options?: Record<string, boolean | number | string>;
56
+ auth?: DefaultsAdapterAuthConfig;
57
+ }
58
+ /** Shape of `config.json`'s top-level `defaults` block. */
59
+ interface SpawnDefaultsConfig {
60
+ skills?: string[];
61
+ options?: Record<string, boolean | number | string>;
62
+ adapters?: Record<string, DefaultsAdapterConfig>;
63
+ /** Depth cutoff for the role-derived default (see `resolveRole` in
64
+ * `role.ts`) applied when an `agent_start` call omits `role`:
65
+ * `depth < cutoff` → supervisor, `depth >= cutoff` → executor.
66
+ * Default 1 (root spawns keep today's unrestricted behaviour; any
67
+ * spawn made THROUGH an orchestrator defaults to executor). Tune
68
+ * this up (e.g. to a large number) to restore the old permissive
69
+ * behaviour for existing deep spawns wholesale. */
70
+ defaultRoleDepthCutoff?: number;
71
+ /** Trust-boundary cap on pack-carried roles (see `role-registry.ts`'s
72
+ * `loadRoleRegistry`): a role pack whose `toolPolicy.delegation` is
73
+ * `"allow"` at a level ABOVE this cap has it forced to `"deny"` —
74
+ * the pack can still declare the intent, the daemon just refuses to
75
+ * grant it. Lets an operator install third-party role packs without
76
+ * trusting every one of them to self-grant delegation. Undefined
77
+ * (default) ⇒ no cap, any pack-declared level may carry
78
+ * `delegation: "allow"` (back-compat: #214 had no such knob). */
79
+ maxGrantableDelegation?: number;
80
+ /** Default per-session Langfuse tracing opt-in when an `agent_start` call
81
+ * omits `trace`. Default false — sessions trace only when they opt in or
82
+ * this is on. See `filterSessionObserver` / `SpawnAgentInput.trace`. */
83
+ langfuseTracing?: boolean;
84
+ /** Redactor slug applied to traced session content before it's sent to
85
+ * Langfuse (see `@agentproto/redaction`'s registry). Default "secrets"
86
+ * (deny-list by key + value-scan for secret shapes). */
87
+ traceRedactor?: string;
88
+ }
89
+ interface ResolveSpawnDefaultsInput {
90
+ /** Explicit-call `skills`. Undefined ⇒ caller expressed no preference,
91
+ * fall through to the config union. Provided ⇒ replaces it outright. */
92
+ skills?: string[];
93
+ /** Explicit-call AIP-45 `options` map — wins per-key over both the
94
+ * global and per-adapter config defaults. */
95
+ options?: Record<string, boolean | number | string>;
96
+ /** Explicit-call `agent_start.auth` override. `mode` wins over
97
+ * `defaults.adapters.<slug>.auth.mode`; the credential field matching the
98
+ * RESOLVED mode wins over the matching config field. Undefined ⇒ falls
99
+ * through entirely to the per-adapter config default. */
100
+ auth?: DefaultsAdapterAuthConfig;
101
+ }
102
+ interface ResolvedSpawnDefaults {
103
+ skills: string[];
104
+ options: Record<string, boolean | number | string>;
105
+ /** RAW billing-auth material (config precedence applied) — fed to the
106
+ * descriptor-aware {@link resolveAuthSpec}, which owns the final mode /
107
+ * env / scrub / credential-source decision. Both candidate credentials
108
+ * are surfaced (NOT collapsed to one), since the ordered-mode selection
109
+ * needs to know which are available before it picks the mode. */
110
+ auth: ResolvedSpawnAuthMaterial;
111
+ }
112
+ interface ResolvedSpawnAuthMaterial {
113
+ /** Operator-requested mode (per-spawn > per-adapter config), or undefined
114
+ * ⇒ let the resolver pick by ordered preference. */
115
+ requestedMode?: "subscription" | "api-key";
116
+ /** True when the operator explicitly configured `auth` (per-spawn OR in
117
+ * `defaults.adapters.<slug>.auth`). The ONLY way to tell "set mode, no
118
+ * key" (fail-fast) from "set nothing" (ambient) — both give no credential.
119
+ * DECISION 5. */
120
+ explicit: boolean;
121
+ /** Subscription bearer token (per-spawn > config), if configured. */
122
+ subscriptionCredential?: string;
123
+ /** Explicit API key (per-spawn > config), if configured — distinct from the
124
+ * providers.json store key the resolver fetches separately. */
125
+ apiKeyCredential?: string;
126
+ /** Per-spawn provider pin, if given. */
127
+ provider?: CatalogProvider;
128
+ }
129
+ declare function resolveSpawnDefaults(defaults: SpawnDefaultsConfig | undefined, adapterSlug: string, input: ResolveSpawnDefaultsInput): ResolvedSpawnDefaults;
130
+ /**
131
+ * Derive a SAFE, non-secret fingerprint for a resolved auth credential —
132
+ * NEVER the raw value — for recording on the session descriptor / surfacing
133
+ * in `agentproto sessions --watch` and `agent_sessions_list` (the
134
+ * "verifiability" requirement: answer "what was used" without exposing the
135
+ * secret). Format: `<mode> · <shape-prefix>…<last4>` when the shape is known
136
+ * (e.g. `subscription · sk-ant-oat…3f9c`), else `<mode> · …<last4>`.
137
+ *
138
+ * The shape marker is matched from a PUBLIC key-prefix table (longest-match),
139
+ * not derived from `mode` — see {@link CREDENTIAL_FINGERPRINT_PREFIXES}. Only
140
+ * the matched prefix + the last 4 characters are ever surfaced, never the
141
+ * middle (mirrors GitHub's `ghp_…abcd` style).
142
+ */
143
+ declare function credentialFingerprint(mode: "subscription" | "api-key", credential: string): string;
144
+ /**
145
+ * The adapter's billing-auth capability, projected from its AIP-45 manifest
146
+ * (`provider` / `authEnforce` / `authSubscription`) by the host resolver. The
147
+ * runtime reads THIS, never the manifest directly — keeping the LLM-catalog
148
+ * coupling in the runtime and the driver mechanical.
149
+ */
150
+ interface AdapterAuthDescriptor {
151
+ /** FIXED provider for a single-provider adapter; omitted for by-model
152
+ * routers (provider then derives from the requested model). */
153
+ provider?: CatalogProvider;
154
+ /** Enforcement policy — `"always"` engages every spawn (claude-code's
155
+ * #312 fail-fast); `"when-configured"` (default) only when `explicit`. */
156
+ authEnforce?: "always" | "when-configured";
157
+ /** Subscription (OAuth/bearer) support. Presence ⇒ the adapter supports
158
+ * `"subscription"` mode. Mirrors the driver's `AgentCliAuthSubscription`. */
159
+ authSubscription?: {
160
+ setEnv: string;
161
+ conflictEnv?: string[];
162
+ unsetEnvAdd?: string[];
163
+ };
164
+ }
165
+ /** The fully-resolved spec the driver applies mechanically. Structurally
166
+ * matches `@agentproto/driver-agent-cli`'s `ResolvedAuthSpec` (each package
167
+ * owns its own copy; the object flows across the boundary by shape). */
168
+ interface ResolvedAuthSpec {
169
+ mode: "subscription" | "api-key";
170
+ credential?: string;
171
+ setEnv: string;
172
+ unsetEnv: string[];
173
+ explicit: boolean;
174
+ enforce: "always" | "when-configured";
175
+ }
176
+ /** Where the resolved credential came from — the observable billing axis
177
+ * (DECISION 10②), never inferred. */
178
+ type CredentialSource = "explicit-config" | "providers-store" | "none";
179
+ /**
180
+ * The OBSERVABLE echo (DECISION 9③ / 10②) — recorded on the session
181
+ * descriptor so a verifier checks the RESOLUTION, never the model's
182
+ * self-report. Never carries the raw credential (only its fingerprint).
183
+ */
184
+ interface AuthEcho {
185
+ provider: CatalogProvider;
186
+ authMode: "subscription" | "api-key";
187
+ credentialSource: CredentialSource;
188
+ setEnv: string;
189
+ fingerprint?: string;
190
+ }
191
+ /**
192
+ * Thrown when the operator requested a billing mode the adapter can't serve —
193
+ * today only `"subscription"` on an adapter with no `authSubscription`. A
194
+ * LOUD, distinct failure (DECISION 4②), never a silent downgrade to api-key.
195
+ */
196
+ declare class AuthResolutionError extends Error {
197
+ readonly code = "unsupported_auth_mode";
198
+ constructor(message: string);
199
+ }
200
+ interface ResolveAuthSpecInput {
201
+ descriptor: AdapterAuthDescriptor;
202
+ /** `input.model ?? adapter default model` — for model-derived provider. */
203
+ model?: string;
204
+ /** Per-spawn provider pin (`input.auth.provider`). */
205
+ requestedProvider?: CatalogProvider;
206
+ /** Operator-requested mode; undefined ⇒ ordered preference. */
207
+ requestedMode?: "subscription" | "api-key";
208
+ /** Operator explicitly configured `auth` (DECISION 5). */
209
+ explicit: boolean;
210
+ /** Subscription bearer credential, if configured. */
211
+ subscriptionCredential?: string;
212
+ /** Explicit api-key credential from config, if configured. */
213
+ apiKeyConfigCredential?: string;
214
+ /** api-key credential from `providers.json` (fetched by the caller). */
215
+ apiKeyStoreCredential?: string;
216
+ }
217
+ /**
218
+ * THE billing-auth resolver (DECISIONS 4, 6, 9, 10). Pure: given the adapter
219
+ * descriptor + raw config material + (caller-fetched) store key, it decides
220
+ * the provider, the mode (ordered — subscription over api-key when a
221
+ * subscription credential is present; a requested-but-unsupported mode throws
222
+ * `unsupported_auth_mode`), the env var to SET, the derived SCRUB set, and the
223
+ * credential + its source. Returns the driver `spec` + the observable `echo`,
224
+ * or `undefined` when no provider resolves (⇒ ambient, no injection — never
225
+ * guess). NEVER falls back to a default provider/model. Fail-loud on a
226
+ * configured-but-missing credential is deferred to the driver's mechanical
227
+ * apply (it engages then throws `missing_auth_credential`), so the `explicit`
228
+ * / `enforce` signals are carried through on the spec.
229
+ */
230
+ declare function resolveAuthSpec(input: ResolveAuthSpecInput): {
231
+ spec: ResolvedAuthSpec;
232
+ echo: AuthEcho;
233
+ } | undefined;
234
+ /** Manifest-declared AIP-45 option id + type, the minimum an adapter
235
+ * resolver needs to expose for `normalizeSkillsOption` below. Mirrors
236
+ * `AgentCliOption`'s `id`/`type` fields without importing
237
+ * `@agentproto/driver-agent-cli` into the runtime package. */
238
+ interface DeclaredAdapterOption {
239
+ id: string;
240
+ type: "boolean" | "integer" | "string" | "enum";
241
+ }
242
+ /**
243
+ * Fold the resolved `skills` list into `options.skills` using whatever
244
+ * shape the adapter's manifest declares for that option id (today, only
245
+ * `type: "string"` exists for a skills-shaped option — e.g. hermes'
246
+ * comma-joined `--skills a,b`). Adapters with no declared `skills` option
247
+ * (e.g. claude-code, which auto-discovers from `~/.claude/skills`) are a
248
+ * documented no-op — the effective skills list has nowhere to go, so it's
249
+ * dropped rather than guessing a flag the manifest didn't declare.
250
+ *
251
+ * An `options.skills` already present (from config defaults or the
252
+ * explicit call) is respected as-is and never overwritten here.
253
+ */
254
+ declare function normalizeSkillsOption(skills: string[], options: Record<string, boolean | number | string>, declaredOptions: readonly DeclaredAdapterOption[] | undefined): Record<string, boolean | number | string>;
255
+
256
+ export { type AdapterAuthDescriptor as A, type CredentialSource as C, type DeclaredAdapterOption as D, type ResolvedAuthSpec as R, type SpawnDefaultsConfig as S, type AuthEcho as a, AuthResolutionError as b, type DefaultsAdapterAuthConfig as c, type DefaultsAdapterConfig as d, type ResolvedSpawnAuthMaterial as e, type ResolvedSpawnDefaults as f, credentialFingerprint as g, resolveSpawnDefaults as h, normalizeSkillsOption as n, resolveAuthSpec as r };
@@ -74,8 +74,12 @@ declare function addWorkspace(config: WorkspacesConfig, input: {
74
74
  declare function removeWorkspace(config: WorkspacesConfig, slug: string): WorkspacesConfig;
75
75
  declare function setActiveWorkspace(config: WorkspacesConfig, slug: string): WorkspacesConfig;
76
76
  declare function findWorkspace(config: WorkspacesConfig, slug: string): WorkspaceEntry | undefined;
77
+ /** Find a workspace whose path is an ancestor of (or equal to) the
78
+ * given directory. Returns the most specific match (longest path)
79
+ * when multiple workspaces nest under the same root. */
80
+ declare function findWorkspaceByPath(config: WorkspacesConfig, dir: string): WorkspaceEntry | undefined;
77
81
  /** The active workspace, or undefined when none is registered. Pure
78
82
  * convenience to avoid re-implementing the lookup at every caller. */
79
83
  declare function getActiveWorkspace(config: WorkspacesConfig): WorkspaceEntry | undefined;
80
84
 
81
- export { DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_PATH, WORKSPACES_CONFIG_VERSION, type WorkspaceEntry, type WorkspacesConfig, addWorkspace, findWorkspace, getActiveWorkspace, loadWorkspacesConfig, removeWorkspace, sanitizeSlug, saveWorkspacesConfig, setActiveWorkspace };
85
+ export { DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_PATH, WORKSPACES_CONFIG_VERSION, type WorkspaceEntry, type WorkspacesConfig, addWorkspace, findWorkspace, findWorkspaceByPath, getActiveWorkspace, loadWorkspacesConfig, removeWorkspace, sanitizeSlug, saveWorkspacesConfig, setActiveWorkspace };
@@ -93,6 +93,11 @@ function setActiveWorkspace(config, slug) {
93
93
  function findWorkspace(config, slug) {
94
94
  return config.workspaces.find((w) => w.slug === sanitizeSlug(slug));
95
95
  }
96
+ function findWorkspaceByPath(config, dir) {
97
+ const resolved = resolve(dir);
98
+ const candidates = config.workspaces.filter((w) => resolved.startsWith(resolve(w.path) + "/") || resolved === resolve(w.path)).sort((a, b) => b.path.length - a.path.length);
99
+ return candidates[0];
100
+ }
96
101
  function getActiveWorkspace(config) {
97
102
  if (!config.active) return config.workspaces[0];
98
103
  return findWorkspace(config, config.active) ?? config.workspaces[0];
@@ -131,6 +136,6 @@ function normalizeConfig(parsed) {
131
136
  return out;
132
137
  }
133
138
 
134
- export { DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_PATH, WORKSPACES_CONFIG_VERSION, addWorkspace, findWorkspace, getActiveWorkspace, loadWorkspacesConfig, removeWorkspace, sanitizeSlug, saveWorkspacesConfig, setActiveWorkspace };
139
+ export { DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_PATH, WORKSPACES_CONFIG_VERSION, addWorkspace, findWorkspace, findWorkspaceByPath, getActiveWorkspace, loadWorkspacesConfig, removeWorkspace, sanitizeSlug, saveWorkspacesConfig, setActiveWorkspace };
135
140
  //# sourceMappingURL=workspaces-config.mjs.map
136
141
  //# sourceMappingURL=workspaces-config.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/workspaces-config.ts"],"names":["fs"],"mappings":"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","file":"workspaces-config.mjs","sourcesContent":["/**\n * `~/.agentproto/workspaces.json` — single source of truth for the\n * local agentproto control plane. Tracks which directories the user\n * has opted into as agentproto workspaces, plus which one is \"active\"\n * (the daemon's default working set when no workspace is named in\n * a tool call).\n *\n * Why a config file instead of recreating an MCP entry per workspace:\n * - One daemon = one MCP entry in the user's IDE (clean to read in\n * `claude mcp list`, no `dapper_willow` / `noble_lantern` spam).\n * - The daemon walks `workspaces[]` at boot and exposes them all;\n * tools that need a target take an optional `workspace` arg and\n * fall back to `active` when omitted.\n * - The CLI (`agentproto workspace add/list/remove/use`), the daemon,\n * and the guilde-web onboarding dialog all read+write the same\n * file, so changes from one surface are immediately visible to\n * the others.\n *\n * File layout is intentionally boring (small, hand-editable JSON) —\n * future fields go on top, never break v1 readers.\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, isAbsolute, resolve } from \"node:path\"\n\nexport const WORKSPACES_CONFIG_VERSION = 1 as const\n\nexport interface WorkspaceEntry {\n /** Stable handle — what tools accept as `workspace`. Lowercase\n * ASCII, hyphen-only. The CLI sanitises arbitrary names down to\n * this shape so paste-from-finder Just Works. */\n slug: string\n /** Absolute filesystem path. Required absolute so the daemon can\n * serve the workspace regardless of its own cwd. */\n path: string\n /** ISO-8601 — first time the entry was added. Pure metadata, no\n * behavioural impact. */\n addedAt: string\n /** Last time the entry was touched (renamed, path edited, used as\n * active). Lets the CLI sort by recency without an N+1 stat. */\n updatedAt: string\n /** Free-text label the user can attach so workspaces with similar\n * slugs stay distinguishable in `agentproto workspace list`. */\n label?: string\n}\n\nexport interface WorkspacesConfig {\n version: typeof WORKSPACES_CONFIG_VERSION\n /** Active workspace slug — daemon defaults to this when no\n * `workspace` arg is passed. May be undefined when no workspaces\n * are registered yet. */\n active?: string\n workspaces: WorkspaceEntry[]\n}\n\nexport const DEFAULT_CONFIG_DIR = (): string =>\n resolve(homedir(), \".agentproto\")\nexport const DEFAULT_CONFIG_PATH = (): string =>\n resolve(DEFAULT_CONFIG_DIR(), \"workspaces.json\")\n\n/** Reduce arbitrary user input to a safe slug. Aligns with what most\n * tooling validates against (`/^[a-z0-9][a-z0-9-]{0,63}$/`) so the\n * daemon never has to encode it for URLs or filesystem paths. */\nexport function sanitizeSlug(input: string): string {\n const trimmed = input.trim().toLowerCase()\n const cleaned = trimmed\n .replace(/[^a-z0-9_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 64)\n // Edge case: input was entirely non-conforming — fall back to a\n // generic so we never return an empty string.\n return cleaned || \"workspace\"\n}\n\n/** Read the config from disk. Returns an empty config (no workspaces,\n * no active) when the file is missing — this is the legitimate\n * first-boot state, not an error. */\nexport async function loadWorkspacesConfig(\n path: string = DEFAULT_CONFIG_PATH()\n): Promise<WorkspacesConfig> {\n let raw: string\n try {\n raw = await fs.readFile(path, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] }\n }\n throw err\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch (err) {\n throw new Error(\n `agentproto: ${path} is not valid JSON (${\n err instanceof Error ? err.message : String(err)\n }). Delete the file or fix it manually.`\n )\n }\n return normalizeConfig(parsed)\n}\n\n/** Write the config back. Creates the parent directory if missing.\n * Atomic-ish: writes to a temp file first, then renames over the\n * target — avoids the partial-write window where a concurrent reader\n * sees half a JSON object. */\nexport async function saveWorkspacesConfig(\n config: WorkspacesConfig,\n path: string = DEFAULT_CONFIG_PATH()\n): Promise<void> {\n const normalized = normalizeConfig(config)\n await fs.mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.tmp.${process.pid}`\n await fs.writeFile(tmp, JSON.stringify(normalized, null, 2) + \"\\n\", \"utf8\")\n await fs.rename(tmp, path)\n}\n\n/**\n * Pure helpers — caller manages persistence. Returns a NEW config\n * (never mutates) so callers can compose multiple updates before\n * a single save.\n */\nexport function addWorkspace(\n config: WorkspacesConfig,\n input: { slug: string; path: string; label?: string }\n): WorkspacesConfig {\n if (!isAbsolute(input.path)) {\n throw new Error(\n `addWorkspace: path must be absolute, got \"${input.path}\".`\n )\n }\n const slug = sanitizeSlug(input.slug)\n const now = new Date().toISOString()\n const existingIdx = config.workspaces.findIndex(w => w.slug === slug)\n const next: WorkspaceEntry = existingIdx >= 0\n ? {\n ...config.workspaces[existingIdx]!,\n path: input.path,\n ...(input.label !== undefined ? { label: input.label } : {}),\n updatedAt: now,\n }\n : {\n slug,\n path: input.path,\n ...(input.label ? { label: input.label } : {}),\n addedAt: now,\n updatedAt: now,\n }\n const workspaces = [...config.workspaces]\n if (existingIdx >= 0) workspaces[existingIdx] = next\n else workspaces.push(next)\n // First-add becomes active automatically — saves the user a\n // separate `workspace use` call right after a fresh `add`.\n const active = config.active ?? slug\n return { ...config, workspaces, active }\n}\n\nexport function removeWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspacesConfig {\n const sanitised = sanitizeSlug(slug)\n const workspaces = config.workspaces.filter(w => w.slug !== sanitised)\n // If the removed workspace was active, hand off to whatever's\n // first — predictable and avoids leaving the daemon in an\n // unreachable state when the user removes the only one.\n let active = config.active\n if (active === sanitised) {\n active = workspaces[0]?.slug\n }\n const next: WorkspacesConfig = { ...config, workspaces }\n if (active !== undefined) next.active = active\n else delete next.active\n return next\n}\n\nexport function setActiveWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspacesConfig {\n const sanitised = sanitizeSlug(slug)\n if (!config.workspaces.some(w => w.slug === sanitised)) {\n throw new Error(\n `setActiveWorkspace: no workspace registered with slug \"${sanitised}\". ` +\n `Run \\`agentproto workspace add <path> --slug ${sanitised}\\` first.`\n )\n }\n return { ...config, active: sanitised }\n}\n\nexport function findWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspaceEntry | undefined {\n return config.workspaces.find(w => w.slug === sanitizeSlug(slug))\n}\n\n/** The active workspace, or undefined when none is registered. Pure\n * convenience to avoid re-implementing the lookup at every caller. */\nexport function getActiveWorkspace(\n config: WorkspacesConfig\n): WorkspaceEntry | undefined {\n if (!config.active) return config.workspaces[0]\n return findWorkspace(config, config.active) ?? config.workspaces[0]\n}\n\n/**\n * Coerce arbitrary parsed JSON into a valid config. Skips entries\n * with missing required fields rather than throwing — partial recovery\n * is friendlier than \"your config file is corrupted, here's a stack\n * trace\". Logs nothing here; the CLI shells that consume this should\n * decide whether to surface a warning.\n */\nfunction normalizeConfig(parsed: unknown): WorkspacesConfig {\n if (!parsed || typeof parsed !== \"object\") {\n return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] }\n }\n const obj = parsed as Record<string, unknown>\n const workspaces: WorkspaceEntry[] = []\n if (Array.isArray(obj.workspaces)) {\n for (const entry of obj.workspaces) {\n if (!entry || typeof entry !== \"object\") continue\n const e = entry as Record<string, unknown>\n const slug = typeof e.slug === \"string\" ? sanitizeSlug(e.slug) : \"\"\n const path = typeof e.path === \"string\" ? e.path : \"\"\n if (!slug || !path) continue\n const addedAt =\n typeof e.addedAt === \"string\" ? e.addedAt : new Date().toISOString()\n const updatedAt = typeof e.updatedAt === \"string\" ? e.updatedAt : addedAt\n const we: WorkspaceEntry = { slug, path, addedAt, updatedAt }\n if (typeof e.label === \"string\" && e.label.trim()) {\n we.label = e.label.trim()\n }\n workspaces.push(we)\n }\n }\n // De-dupe by slug (last wins) — the file is hand-editable so a\n // typo could have produced two entries with the same slug.\n const dedup = new Map<string, WorkspaceEntry>()\n for (const w of workspaces) dedup.set(w.slug, w)\n const finalList = Array.from(dedup.values())\n const active =\n typeof obj.active === \"string\" && finalList.some(w => w.slug === obj.active)\n ? (obj.active as string)\n : finalList[0]?.slug\n const out: WorkspacesConfig = {\n version: WORKSPACES_CONFIG_VERSION,\n workspaces: finalList,\n }\n if (active !== undefined) out.active = active\n return out\n}\n"]}
1
+ {"version":3,"sources":["../src/workspaces-config.ts"],"names":["fs"],"mappings":"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","file":"workspaces-config.mjs","sourcesContent":["/**\n * `~/.agentproto/workspaces.json` — single source of truth for the\n * local agentproto control plane. Tracks which directories the user\n * has opted into as agentproto workspaces, plus which one is \"active\"\n * (the daemon's default working set when no workspace is named in\n * a tool call).\n *\n * Why a config file instead of recreating an MCP entry per workspace:\n * - One daemon = one MCP entry in the user's IDE (clean to read in\n * `claude mcp list`, no `dapper_willow` / `noble_lantern` spam).\n * - The daemon walks `workspaces[]` at boot and exposes them all;\n * tools that need a target take an optional `workspace` arg and\n * fall back to `active` when omitted.\n * - The CLI (`agentproto workspace add/list/remove/use`), the daemon,\n * and the guilde-web onboarding dialog all read+write the same\n * file, so changes from one surface are immediately visible to\n * the others.\n *\n * File layout is intentionally boring (small, hand-editable JSON) —\n * future fields go on top, never break v1 readers.\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, isAbsolute, resolve } from \"node:path\"\n\nexport const WORKSPACES_CONFIG_VERSION = 1 as const\n\nexport interface WorkspaceEntry {\n /** Stable handle — what tools accept as `workspace`. Lowercase\n * ASCII, hyphen-only. The CLI sanitises arbitrary names down to\n * this shape so paste-from-finder Just Works. */\n slug: string\n /** Absolute filesystem path. Required absolute so the daemon can\n * serve the workspace regardless of its own cwd. */\n path: string\n /** ISO-8601 — first time the entry was added. Pure metadata, no\n * behavioural impact. */\n addedAt: string\n /** Last time the entry was touched (renamed, path edited, used as\n * active). Lets the CLI sort by recency without an N+1 stat. */\n updatedAt: string\n /** Free-text label the user can attach so workspaces with similar\n * slugs stay distinguishable in `agentproto workspace list`. */\n label?: string\n}\n\nexport interface WorkspacesConfig {\n version: typeof WORKSPACES_CONFIG_VERSION\n /** Active workspace slug — daemon defaults to this when no\n * `workspace` arg is passed. May be undefined when no workspaces\n * are registered yet. */\n active?: string\n workspaces: WorkspaceEntry[]\n}\n\nexport const DEFAULT_CONFIG_DIR = (): string =>\n resolve(homedir(), \".agentproto\")\nexport const DEFAULT_CONFIG_PATH = (): string =>\n resolve(DEFAULT_CONFIG_DIR(), \"workspaces.json\")\n\n/** Reduce arbitrary user input to a safe slug. Aligns with what most\n * tooling validates against (`/^[a-z0-9][a-z0-9-]{0,63}$/`) so the\n * daemon never has to encode it for URLs or filesystem paths. */\nexport function sanitizeSlug(input: string): string {\n const trimmed = input.trim().toLowerCase()\n const cleaned = trimmed\n .replace(/[^a-z0-9_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 64)\n // Edge case: input was entirely non-conforming — fall back to a\n // generic so we never return an empty string.\n return cleaned || \"workspace\"\n}\n\n/** Read the config from disk. Returns an empty config (no workspaces,\n * no active) when the file is missing — this is the legitimate\n * first-boot state, not an error. */\nexport async function loadWorkspacesConfig(\n path: string = DEFAULT_CONFIG_PATH()\n): Promise<WorkspacesConfig> {\n let raw: string\n try {\n raw = await fs.readFile(path, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] }\n }\n throw err\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch (err) {\n throw new Error(\n `agentproto: ${path} is not valid JSON (${\n err instanceof Error ? err.message : String(err)\n }). Delete the file or fix it manually.`\n )\n }\n return normalizeConfig(parsed)\n}\n\n/** Write the config back. Creates the parent directory if missing.\n * Atomic-ish: writes to a temp file first, then renames over the\n * target — avoids the partial-write window where a concurrent reader\n * sees half a JSON object. */\nexport async function saveWorkspacesConfig(\n config: WorkspacesConfig,\n path: string = DEFAULT_CONFIG_PATH()\n): Promise<void> {\n const normalized = normalizeConfig(config)\n await fs.mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.tmp.${process.pid}`\n await fs.writeFile(tmp, JSON.stringify(normalized, null, 2) + \"\\n\", \"utf8\")\n await fs.rename(tmp, path)\n}\n\n/**\n * Pure helpers — caller manages persistence. Returns a NEW config\n * (never mutates) so callers can compose multiple updates before\n * a single save.\n */\nexport function addWorkspace(\n config: WorkspacesConfig,\n input: { slug: string; path: string; label?: string }\n): WorkspacesConfig {\n if (!isAbsolute(input.path)) {\n throw new Error(\n `addWorkspace: path must be absolute, got \"${input.path}\".`\n )\n }\n const slug = sanitizeSlug(input.slug)\n const now = new Date().toISOString()\n const existingIdx = config.workspaces.findIndex(w => w.slug === slug)\n const next: WorkspaceEntry = existingIdx >= 0\n ? {\n ...config.workspaces[existingIdx]!,\n path: input.path,\n ...(input.label !== undefined ? { label: input.label } : {}),\n updatedAt: now,\n }\n : {\n slug,\n path: input.path,\n ...(input.label ? { label: input.label } : {}),\n addedAt: now,\n updatedAt: now,\n }\n const workspaces = [...config.workspaces]\n if (existingIdx >= 0) workspaces[existingIdx] = next\n else workspaces.push(next)\n // First-add becomes active automatically — saves the user a\n // separate `workspace use` call right after a fresh `add`.\n const active = config.active ?? slug\n return { ...config, workspaces, active }\n}\n\nexport function removeWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspacesConfig {\n const sanitised = sanitizeSlug(slug)\n const workspaces = config.workspaces.filter(w => w.slug !== sanitised)\n // If the removed workspace was active, hand off to whatever's\n // first — predictable and avoids leaving the daemon in an\n // unreachable state when the user removes the only one.\n let active = config.active\n if (active === sanitised) {\n active = workspaces[0]?.slug\n }\n const next: WorkspacesConfig = { ...config, workspaces }\n if (active !== undefined) next.active = active\n else delete next.active\n return next\n}\n\nexport function setActiveWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspacesConfig {\n const sanitised = sanitizeSlug(slug)\n if (!config.workspaces.some(w => w.slug === sanitised)) {\n throw new Error(\n `setActiveWorkspace: no workspace registered with slug \"${sanitised}\". ` +\n `Run \\`agentproto workspace add <path> --slug ${sanitised}\\` first.`\n )\n }\n return { ...config, active: sanitised }\n}\n\nexport function findWorkspace(\n config: WorkspacesConfig,\n slug: string\n): WorkspaceEntry | undefined {\n return config.workspaces.find(w => w.slug === sanitizeSlug(slug))\n}\n\n/** Find a workspace whose path is an ancestor of (or equal to) the\n * given directory. Returns the most specific match (longest path)\n * when multiple workspaces nest under the same root. */\nexport function findWorkspaceByPath(\n config: WorkspacesConfig,\n dir: string\n): WorkspaceEntry | undefined {\n const resolved = resolve(dir)\n const candidates = config.workspaces\n .filter(w => resolved.startsWith(resolve(w.path) + \"/\") || resolved === resolve(w.path))\n .sort((a, b) => b.path.length - a.path.length)\n return candidates[0]\n}\n\n/** The active workspace, or undefined when none is registered. Pure\n * convenience to avoid re-implementing the lookup at every caller. */\nexport function getActiveWorkspace(\n config: WorkspacesConfig\n): WorkspaceEntry | undefined {\n if (!config.active) return config.workspaces[0]\n return findWorkspace(config, config.active) ?? config.workspaces[0]\n}\n\n/**\n * Coerce arbitrary parsed JSON into a valid config. Skips entries\n * with missing required fields rather than throwing — partial recovery\n * is friendlier than \"your config file is corrupted, here's a stack\n * trace\". Logs nothing here; the CLI shells that consume this should\n * decide whether to surface a warning.\n */\nfunction normalizeConfig(parsed: unknown): WorkspacesConfig {\n if (!parsed || typeof parsed !== \"object\") {\n return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] }\n }\n const obj = parsed as Record<string, unknown>\n const workspaces: WorkspaceEntry[] = []\n if (Array.isArray(obj.workspaces)) {\n for (const entry of obj.workspaces) {\n if (!entry || typeof entry !== \"object\") continue\n const e = entry as Record<string, unknown>\n const slug = typeof e.slug === \"string\" ? sanitizeSlug(e.slug) : \"\"\n const path = typeof e.path === \"string\" ? e.path : \"\"\n if (!slug || !path) continue\n const addedAt =\n typeof e.addedAt === \"string\" ? e.addedAt : new Date().toISOString()\n const updatedAt = typeof e.updatedAt === \"string\" ? e.updatedAt : addedAt\n const we: WorkspaceEntry = { slug, path, addedAt, updatedAt }\n if (typeof e.label === \"string\" && e.label.trim()) {\n we.label = e.label.trim()\n }\n workspaces.push(we)\n }\n }\n // De-dupe by slug (last wins) — the file is hand-editable so a\n // typo could have produced two entries with the same slug.\n const dedup = new Map<string, WorkspaceEntry>()\n for (const w of workspaces) dedup.set(w.slug, w)\n const finalList = Array.from(dedup.values())\n const active =\n typeof obj.active === \"string\" && finalList.some(w => w.slug === obj.active)\n ? (obj.active as string)\n : finalList[0]?.slug\n const out: WorkspacesConfig = {\n version: WORKSPACES_CONFIG_VERSION,\n workspaces: finalList,\n }\n if (active !== undefined) out.active = active\n return out\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/runtime",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "@agentproto/runtime — long-running gateway that turns an agentproto workspace into a live runtime. Composes @agentproto/mcp-server (CRUD verbs) with HTTP transport, HEARTBEAT.md autonomy loop, and append-only conversation persistence. Drop a workspace dir, point your MCP client at it, and the agent ticks on its own.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -19,7 +19,7 @@
19
19
  "bugs": {
20
20
  "url": "https://github.com/agentproto/ts/issues"
21
21
  },
22
- "license": "MIT",
22
+ "license": "Apache-2.0",
23
23
  "type": "module",
24
24
  "main": "dist/index.mjs",
25
25
  "module": "dist/index.mjs",
@@ -70,6 +70,11 @@
70
70
  "types": "./dist/resume-strategies.d.ts",
71
71
  "import": "./dist/resume-strategies.mjs",
72
72
  "default": "./dist/resume-strategies.mjs"
73
+ },
74
+ "./session-story": {
75
+ "types": "./dist/session-story.d.ts",
76
+ "import": "./dist/session-story.mjs",
77
+ "default": "./dist/session-story.mjs"
73
78
  }
74
79
  },
75
80
  "files": [
@@ -86,23 +91,25 @@
86
91
  "gray-matter": "^4.0.3",
87
92
  "ws": "^8.18.0",
88
93
  "zod": "^4.4.3",
89
- "@agentproto/acp": "0.4.0",
90
- "@agentproto/workflow": "0.1.0-alpha.1",
91
- "@agentproto/workflow-loader": "0.1.0-alpha.0",
92
- "@agentproto/workflow-runtime": "0.3.0",
93
- "@agentproto/provider-kit": "0.2.0",
94
- "@agentproto/provider-presets": "0.2.0",
95
- "@agentproto/eval-reporters": "0.2.0",
96
- "@agentproto/agent": "0.2.0",
97
- "@agentproto/manifest": "0.2.0",
98
- "@agentproto/mcp-server": "0.2.1",
99
- "@agentproto/model-catalog": "0.2.0",
100
- "@agentproto/redaction": "0.2.0",
101
- "@agentproto/sandbox": "0.1.0",
102
- "@agentproto/telemetry-langfuse": "0.2.0"
94
+ "@agentproto/acp": "0.5.0",
95
+ "@agentproto/workflow": "0.1.0",
96
+ "@agentproto/workflow-loader": "0.1.0",
97
+ "@agentproto/workflow-runtime": "0.4.0",
98
+ "@agentproto/provider-kit": "0.2.1",
99
+ "@agentproto/eval-reporters": "0.2.1",
100
+ "@agentproto/provider-presets": "0.3.0",
101
+ "@agentproto/agent": "0.2.1",
102
+ "@agentproto/manifest": "0.2.1",
103
+ "@agentproto/mcp-server": "0.2.2",
104
+ "@agentproto/model-catalog": "0.3.0",
105
+ "@agentproto/providers-store": "0.2.0",
106
+ "@agentproto/redaction": "0.2.1",
107
+ "@agentproto/sandbox": "0.1.1",
108
+ "@agentproto/secrets": "0.2.0",
109
+ "@agentproto/telemetry-langfuse": "0.2.1"
103
110
  },
104
111
  "optionalDependencies": {
105
- "@agentproto/sandbox-e2b": "0.1.0"
112
+ "@agentproto/sandbox-e2b": "0.1.1"
106
113
  },
107
114
  "devDependencies": {
108
115
  "@types/node": "^25.6.2",
@@ -110,8 +117,9 @@
110
117
  "tsup": "^8.5.1",
111
118
  "typescript": "^5.9.3",
112
119
  "vitest": "^3.2.4",
113
- "@agentproto/driver": "0.1.2",
114
- "@agentproto/tool": "0.2.0",
120
+ "@agentproto/driver": "0.1.3",
121
+ "@agentproto/rendezvous": "0.2.0",
122
+ "@agentproto/tool": "0.2.1",
115
123
  "@agentproto/tooling": "0.1.0-alpha.0"
116
124
  },
117
125
  "scripts": {
@@ -1,61 +0,0 @@
1
- /**
2
- * Pure resolver for `~/.agentproto/config.json`'s `defaults` block —
3
- * computes the effective `skills` + `options` for an `agent_start` spawn
4
- * before adapter-specific normalization. No fs, no adapter I/O, so it's
5
- * unit-testable in isolation from `session-spawn.ts` (which owns the fs
6
- * read + the adapter-manifest lookup).
7
- *
8
- * Precedence (lowest → highest): global `defaults` < `defaults.adapters.
9
- * <slug>` < the explicit `agent_start` call.
10
- * - `options`: shallow-merged maps, later (higher-precedence) keys win.
11
- * - `skills`: global ∪ per-adapter when the caller didn't pass `skills`
12
- * at all; an explicit `skills` (even `[]`) REPLACES the union rather
13
- * than merging into it — a deliberate exact set, mirroring how an
14
- * explicit `mcpServers: []` opts out of the hermes default in
15
- * `session-spawn.ts`.
16
- */
17
- interface DefaultsAdapterConfig {
18
- skills?: string[];
19
- options?: Record<string, boolean | number | string>;
20
- }
21
- /** Shape of `config.json`'s top-level `defaults` block. */
22
- interface SpawnDefaultsConfig {
23
- skills?: string[];
24
- options?: Record<string, boolean | number | string>;
25
- adapters?: Record<string, DefaultsAdapterConfig>;
26
- /** Depth cutoff for the role-derived default (see `resolveRole` in
27
- * `role.ts`) applied when an `agent_start` call omits `role`:
28
- * `depth < cutoff` → supervisor, `depth >= cutoff` → executor.
29
- * Default 1 (root spawns keep today's unrestricted behaviour; any
30
- * spawn made THROUGH an orchestrator defaults to executor). Tune
31
- * this up (e.g. to a large number) to restore the old permissive
32
- * behaviour for existing deep spawns wholesale. */
33
- defaultRoleDepthCutoff?: number;
34
- /** Trust-boundary cap on pack-carried roles (see `role-registry.ts`'s
35
- * `loadRoleRegistry`): a role pack whose `toolPolicy.delegation` is
36
- * `"allow"` at a level ABOVE this cap has it forced to `"deny"` —
37
- * the pack can still declare the intent, the daemon just refuses to
38
- * grant it. Lets an operator install third-party role packs without
39
- * trusting every one of them to self-grant delegation. Undefined
40
- * (default) ⇒ no cap, any pack-declared level may carry
41
- * `delegation: "allow"` (back-compat: #214 had no such knob). */
42
- maxGrantableDelegation?: number;
43
- /** Default per-session Langfuse tracing opt-in when an `agent_start` call
44
- * omits `trace`. Default false — sessions trace only when they opt in or
45
- * this is on. See `filterSessionObserver` / `SpawnAgentInput.trace`. */
46
- langfuseTracing?: boolean;
47
- /** Redactor slug applied to traced session content before it's sent to
48
- * Langfuse (see `@agentproto/redaction`'s registry). Default "secrets"
49
- * (deny-list by key + value-scan for secret shapes). */
50
- traceRedactor?: string;
51
- }
52
- /** Manifest-declared AIP-45 option id + type, the minimum an adapter
53
- * resolver needs to expose for `normalizeSkillsOption` below. Mirrors
54
- * `AgentCliOption`'s `id`/`type` fields without importing
55
- * `@agentproto/driver-agent-cli` into the runtime package. */
56
- interface DeclaredAdapterOption {
57
- id: string;
58
- type: "boolean" | "integer" | "string" | "enum";
59
- }
60
-
61
- export type { DeclaredAdapterOption as D, SpawnDefaultsConfig as S };