/** * Default wiring — the one async canonical bootstrap (saga d4ks8c.7; ARCHITECTURE §4/§5/§15). * * This is the single default startup path. It: * - independently discovers typed resources (SkillRegistry, ContextFileSnapshot, * AgentDefinitionSnapshot, PermissionRuleSnapshot) and dynamic extension sources, * aggregating typed diagnostics WITHOUT flattening them into `ResourceRecord`; * - constructs an `ExtensionHost` and installs bundled tool/mode/compactor extensions * through `host.install`, registers `recall` and `load_skill` as `ExtensionFunction`s * (the recall factory seam captures a `SessionStore`), and loads trusted dynamic * extensions through `ExtensionLoader`. `ExtensionApi` is the EXCLUSIVE live capability * registration surface — built-ins are never registered directly into * `DefaultToolRegistry`/`ModeController` anymore; * - builds the runtime `ToolRegistry`/`ModeController`/compactor/provider selection from * the host snapshot only; * - applies specialized permission rules in first-match order together with explicitly * supplied `opts.permissions.rules`, constructing `RealPermissionGate` when either is * present (else `AllowAllGate`); * - composes the BASE system prompt in pinned order (base identity → layered context → * available-skills catalog) that `AgentSession` then extends * (active mode → durable activated-skill bodies → latest plan → compaction handoff). * * Provider/model + compaction options are preserved (backward compatible). Resource * discovery is controlled by `opts.resources`: when absent the bootstrap stays hermetic * (pure built-in wiring, empty typed snapshots) so unit/seam tests stay deterministic; the * real CLI/SDK startup paths inject `resources` to make every completed capability live. * * No hardcoded secrets: API keys come from the `apiKey` param or `OPENAI_API_KEY` env * (the provider throws if neither is set). */ import type { AgentSession } from '@/core/session/agent-session'; import type { Rule } from '@/contracts/decision'; import type { PermissionGate } from '@/contracts/permission-gate'; import type { Compactor } from '@/contracts/compactor'; import type { CodeIntelligenceProvider } from '@/contracts/code-intelligence'; import type { Provider } from '@/contracts/provider'; import type { ModelDescriptor } from '@/contracts/model'; import type { ToolRegistry } from '@/contracts/tool-registry'; import type { ExtensionFunction, ExtensionIdentity, ExtensionLoadDiagnostic, ExtensionTrustGate, } from '@/contracts/extension-api'; import type { ManagedContext, CapabilityPolicy, } from '@/contracts/managed-context'; import type { BootstrapContributionFactory } from '@/contracts/bootstrap-contribution'; import { validateToolAllowlist, validateSkillAllowlist, } from '@/contracts/managed-context'; import type { SkillRegistry, SkillRegistrySnapshot, SkillDiagnostic, } from '@/contracts/skill'; import type { ContextFileSnapshot, ContextFileDiagnostic, ContextTrustGate, } from '@/contracts/context-file'; import type { AgentDefinitionSnapshot, AgentDefinitionDiagnostic, AgentDefinitionTrustGate, } from '@/contracts/agent-definition'; import type { PermissionRuleSnapshot, PermissionRuleDiagnostic, PermissionRuleTrustGate, } from '@/contracts/permission-rule'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { AgentSession as AgentSessionClass } from '@/core/session/agent-session'; import { OpenAIProvider } from '@/core/provider/openai'; import { OpenAIResponsesProvider } from '@/core/provider/openai-responses'; import { DefaultToolRegistry } from '@/core/tools/registry'; import { AllowAllGate } from '@/core/permission/gate'; import { RealPermissionGate } from '@/core/permission/real-gate'; import { InProcessEventBus } from '@/core/event-bus'; import { SessionStore } from '@/core/session/store'; import { ModeController } from '@/core/modes/controller'; import { generateId } from '@/core/id'; import { ThresholdCompactionTrigger } from '@/core/compactor/trigger'; import type { CompactionConfig } from '@/contracts/compaction-trigger'; import { DEFAULT_CONTEXT_WINDOW } from '@/contracts/compaction-trigger'; import { ExtensionHost } from '@/core/extensions/host'; import { builtinToolsExtension, builtinModesExtension, builtinCompactorExtension, } from '@/core/extensions/builtin'; import { ExtensionLoader, type ExtensionImporter, type ExtensionLoaderFs, } from '@/core/extensions/loader'; import { createLoadSkillExtension } from '@/core/tools/load-skill'; import { createRecallExtension } from '@/core/tools/recall'; import { FilteredToolRegistry } from '@/core/tools/filtered-registry'; import { FilteredSkillRegistry } from '@/core/agent-skills/filtered-registry'; import { loadSkillRegistry } from '@/core/agent-skills/loader'; import { DefaultSkillRegistry } from '@/core/agent-skills/registry'; import { formatSkillCatalog } from '@/core/agent-skills/catalog'; import { ContextFileLoader, AgentDefinitionLoader, PermissionRuleLoader, defaultAgentDefinitionUserRoots, } from '@/core/resources'; import { nodeResourceFilesystem, type ResourceFilesystem } from '@/core/resources/filesystem'; import { defaultProjectBoundaryResolver, type ProjectBoundaryResolver, } from '@/core/project-boundary'; /** Reasonable default system prompt for a coding agent. */ export const DEFAULT_SYSTEM_PROMPT = 'You are Shard, a coding agent. Use tools to help the user.'; /** Mode names accepted by the default wiring. */ export type DefaultMode = 'plan' | 'build'; /** Resolved triple the provider consumes. Mirrors the L4 `ResolvedModel` shape (L2 * can't import L4 — `CORE_FORBIDDEN` eslint rule — so the type is local). The L4 resolver * (`resolveModel` in `@/cli/config/resolve`) returns `ResolvedModel`; the CLI adapts it * to this triple before injecting via `resolveProvider`. */ export interface ResolvedProviderTriple { /** Model id sent in the request body's `model` field. */ model: string; /** Authentication mode; omitted preserves the legacy bearer default. */ auth?: 'bearer' | 'none'; /** Bearer token, or undefined for none auth. */ apiKey?: string; /** Resolved base URL (trailing slash already stripped by the resolver). */ baseURL?: string; /** * Wire adapter (saga hihsjg). `'responses'` → OpenAI Responses API provider (readable * reasoning summaries); `'chat-completions'` → the OpenAI-compatible provider. Defaults to * `'chat-completions'` when omitted (the historical behavior, safe for compat servers). */ api?: 'chat-completions' | 'responses'; /** Whether the model is a reasoning model (saga hihsjg); opts the Responses provider * into reasoning summaries + the developer role. Ignored for chat-completions. */ reasoning?: boolean; /** * The resolved effective reasoning effort (saga p7m8jp.1). Surfaced so the provider * factory can pass it to the Responses / Chat Completions provider. When absent the * provider applies its own adapter default (Responses currently uses `medium`). */ reasoningEffort?: string; /** * The supported reasoning effort IDs (saga p7m8jp.1). Surfaced so the runtime * authority can serve `list_reasoning_levels` and advertise capabilities. When absent * the model has no selectable effort surface. */ reasoningEfforts?: string[]; } /** * Injected L4 resolver — given {providerKey, modelName}, returns the resolved triple. * The CLI injects `resolveModel` from `@/cli/config` (L4); L2 cannot import it directly * (ARCHITECTURE §16 layer rule), so injection keeps the boundary intact. */ export type ProviderResolver = (input: { providerKey?: string; modelName?: string; }) => ResolvedProviderTriple; /** * Resource discovery options for the canonical bootstrap (d4ks8c.7). When `resources` is * absent on {@link CreateAgentSessionOptions}, discovery is SKIPPED (hermetic, pure * built-in wiring) — the historic `createDefaultCore` behavior unit/seam tests rely on. * The real CLI/SDK startup paths pass `resources` to make every completed capability live. * * Roots are injected (never hard-coded). Precedence is fixed per loader: skills * bundled → user (`~/.shard/skills` + `~/.agents/skills`) → project * (`.shard/skills` + `.agents/skills` up to the git root); context/agent/rule/extension * bundled → user → project (least-specific ancestor → cwd). Project instruction/code * resources require affirmative trust: `projectTrusted` defaults to `false`, so project * SHARD.md/AGENTS.md, agent definitions, `allow` permission rules, skills, and executable * extensions are filtered/skipped (with diagnostics, no code executed) unless the caller * opts in. Bundled/user sources are trusted by being explicitly configured. */ export interface BootstrapResourcesOptions { /** Project cwd. Defaults to `process.cwd()`. Ancestors up to the git root are scanned. */ cwd?: string; /** User home; enables user-layer discovery (`~/.shard/...`, `~/.agents/skills`). */ userHome?: string; /** Bundled roots — ship defaults, lowest precedence. Applied to every specialized loader. */ bundledRoots?: string[]; /** User roots for the context/rule loaders (and the agent loader, for backward * compatibility, when {@link userAgentRoots} is unset). Default `[/.shard]`. */ userRoots?: string[]; /** * User roots for the AGENT-DEFINITION loader only. Definitions are user-authored config, * so their authoritative home is the XDG config dir — not the `~/.shard` state home, which * also holds per-agent session data. Default: {@link defaultAgentDefinitionUserRoots} * (legacy `~/.shard/agents` fallback first, `~/.config/shard/agents` second — config-dir * definitions win same-name collisions). Explicit {@link userRoots} still feeds the agent * loader when this is unset (backward compatibility for callers pinning shared roots). */ userAgentRoots?: string[]; /** User extension roots. Default `[/.shard]` when `userHome` set. */ userExtensionRoots?: string[]; /** Enable `AGENTS.md` compatibility in addition to `SHARD.md`. Default `true`. */ agentsMd?: boolean; /** Affirmative project trust. Default `false` (project resources filtered, no code exec). */ projectTrusted?: boolean; /** Trust gate consulted for project extension sources only. Defaults to `projectTrusted`. */ extensionTrustGate?: ExtensionTrustGate; /** Filesystem seam for resource discovery; defaults to `nodeResourceFilesystem()`. */ fs?: ResourceFilesystem; /** Filesystem seam for dynamic extension discovery; defaults to `nodeExtensionLoaderFs()`. */ extensionFs?: ExtensionLoaderFs; /** * Injectable project-boundary resolver shared across ALL loaders (skills, context, agent * definitions, permission rules, dynamic extensions). When present its canonical top-level * is authoritative for every ancestor walk (a linked worktree's worktree root wins). The * canonical bootstrap defaults to the Git-backed {@link defaultProjectBoundaryResolver} on * the PRODUCTION real-filesystem path (see {@link effectiveBoundaryResolver}); a hermetic * test that injects a fake `fs` stays on the cheap filesystem `.git`-file-or-dir fallback so * no Git is spawned. Explicit injection always wins. */ boundaryResolver?: ProjectBoundaryResolver; /** Injectable module importer for hermetic extension-loader tests. */ importer?: ExtensionImporter; /** Injectable skill registry (a fresh one is created when omitted). */ skillRegistry?: SkillRegistry; } /** Options for `createAgentSession`. */ export interface CreateAgentSessionOptions { /** Filesystem path to the session JSONL file (parent dir is mkdir -p'd). */ sessionPath: string; /** Working directory for tools. Defaults to `process.cwd()`. */ cwd?: string; /** Agent id. Generated via `generateId()` if not supplied. */ agentId?: string; /** Durable session id when it differs from the runtime agent id. */ sessionId?: string; /** Active mode at construction. Defaults to `build`. */ mode?: DefaultMode; /** Base identity system prompt. Defaults to `DEFAULT_SYSTEM_PROMPT`. Layered context + the * available-skills catalog are appended by the bootstrap (saga d4ks8c.7). */ systemPrompt?: string; /** * Opt-in permission policy. When set OR discovered rules exist, `createDefaultCore` * constructs a `RealPermissionGate` sharing the core's `eventBus` + `modeController`, * with grants persisted under `~/.shard/agents//`. Absent and no discovered * rules → `AllowAllGate` (no regression). For a managed root, pinned managed rules are * authoritative and evaluated first; otherwise explicit `opts.permissions.rules` are first, * followed by discovered disk rules in their first-match order. */ permissions?: { rules?: Rule[] }; /** * Compaction wiring (gy5n0k.7, saga dfcvo6). When set, `createDefaultCore` wires a * compactor (from the host snapshot) + `ThresholdCompactionTrigger` with this config * (defaults applied via `applyCompactionDefaults`). Absent → compaction disabled. */ compaction?: Partial; // ── Provider/model resolution (one of the two paths below) ───────────────────── /** Explicit triple — test escape hatch. @deprecated for new callers. */ model?: string; /** API key override (falls back to `OPENAI_API_KEY` env inside the provider). */ apiKey?: string; /** Authentication mode; omitted preserves the legacy bearer default. */ auth?: 'bearer' | 'none'; /** Base URL override (falls back to `OPENAI_BASE_URL` env inside the provider). */ baseURL?: string; /** * Wire adapter (saga hihsjg). `'responses'` selects the OpenAI Responses API provider * (readable reasoning summaries for gpt-5 / o-series); the default `'chat-completions'` * keeps the OpenAI-compatible provider. Optional; the CLI surfaces the resolver's * choice, SDK callers can set it explicitly. */ api?: 'chat-completions' | 'responses'; /** Whether the model is a reasoning model (saga hihsjg); opts the Responses provider * into reasoning summaries + the developer role. Optional. */ reasoning?: boolean; /** * The resolved effective reasoning effort (saga p7m8jp.1). Passed to the provider * factory. When absent the provider applies its own adapter default. */ reasoningEffort?: string; /** * The supported reasoning effort IDs (saga p7m8jp.1). Carried so the runtime authority * can serve `list_reasoning_levels` and advertise capabilities. Optional. */ reasoningEfforts?: string[]; /** Provider key + model alias — resolved via `resolveProvider`. */ providerKey?: string; modelName?: string; /** Injected L4 resolver. Required when `model` is not set. */ resolveProvider?: ProviderResolver; /** * Resource discovery control (d4ks8c.7). Absent → hermetic (no discovery, pure built-in * wiring). The CLI/SDK pass this to make discovered skills/context/agents/rules/extensions * live through the canonical bootstrap. */ resources?: BootstrapResourcesOptions; /** * Managed bootstrap context (saga u4uo9m.3). When set, the bootstrap carries the daemon * context for later L3 extension injection and applies the capability policy. Only an actual * child (`capability.is_child`) receives the fresh read-only child permission gate; roots * retain normal root permission behavior. */ managed?: ManagedBootstrapOptions; /** * Generic injected bootstrap-contribution seam (saga u4uo9m.3 plan item 5 / transport * layering). Each entry is installed through the `ExtensionHost` BEFORE the capability * snapshot is taken, so registered tools/modes/compactors are live and subject to the * capability policy. L4 (the CLI) builds these with the managed context captured in * their closures — L2 never imports L3. Absent → no extra extensions (the historic * built-in + dynamic-discovery wiring). */ extensions?: ReadonlyArray; /** * Generic injected bootstrap-contribution FACTORY (saga u4uo9m.5 plan item 1). Unlike * {@link extensions} (which capture their own data in closures the L4 builds BEFORE this * bootstrap runs), a contribution factory receives the trusted snapshots DISCOVERED BY * this bootstrap plus the pinned {@link ManagedContext}. The canonical bootstrap invokes * it after resource discovery and installs the returned extensions through the * `ExtensionHost` BEFORE the capability snapshot. The concrete L3 implementation lives in * `src/subagent/**` and is imported + injected by the L4 CLI; the core never imports L3. * Consulted when {@link managed} is present, including daemon-backed roots. Absent → no * contribution extensions. */ contributionFactory?: BootstrapContributionFactory; } /** * A bootstrap-time extension the caller injects through the `ExtensionHost` before the * capability snapshot is taken (saga u4uo9m.3). Distinct from dynamic project extensions * (which are code-execution-discovered + trust-gated): these are explicitly supplied by the * L4 wiring (e.g. the bundled L3 subagent tools in saga u4uo9m.5) and trusted by being * explicitly injected. */ export interface BootstrapExtension { identity: ExtensionIdentity; install: ExtensionFunction; } /** * Managed bootstrap options (saga u4uo9m.3). The CLI populates this from a validated * `AgentManifestV1`; L2 consumes only the plain values (no L4 import). The capability's * `is_child` flag determines whether child isolation applies; daemon-backed roots use the * same context without the child gate. */ export interface ManagedBootstrapOptions { /** Managed context carried on {@link DefaultCore.managed} for L3 injection. */ context: ManagedContext; /** Capability policy (tool/skill filtering + whether this is an isolated child). */ capability: CapabilityPolicy; /** * Per-child grant directory for the fresh child `RealPermissionGate`. Defaults to * `~/.shard/agents//` (the historic per-agent dir); the daemon may pin a * daemon-home-relative path so grants are isolated from direct-run agents. */ childGrantBaseDir?: string; /** Optional permission rules pinned from the manifest/definition (evaluated first). */ rules?: Rule[]; } /** Aggregated typed resource diagnostics observable on {@link DefaultCore} (d4ks8c.7). */ export interface BootstrapDiagnostics { readonly skills: ReadonlyArray; readonly context: ReadonlyArray; readonly agents: ReadonlyArray; readonly rules: ReadonlyArray; readonly extensions: ReadonlyArray; /** Host registration diagnostics (tools/modes/compactors/providers/…). */ readonly host: ReadonlyArray; } /** The individually-constructed L2 pieces, for callers that want direct access. */ export interface DefaultCore { /** Active provider resolved from the authoritative ExtensionHost snapshot. */ provider: Provider; /** * Tool registry exposed to the session. A `DefaultToolRegistry` on the direct/test path; * a {@link FilteredToolRegistry} wrapping it when a managed capability policy is in effect * (saga u4uo9m.3). Typed as the `ToolRegistry` seam so callers don't depend on the concrete * class. */ registry: ToolRegistry; gate: PermissionGate; modeController: ModeController; sessionStore: SessionStore; eventBus: InProcessEventBus; agentId: string; sessionId?: string; cwd: string; systemPrompt: string; /** Compactor (from the host snapshot); undefined when compaction is disabled. */ compactor?: Compactor; /** Compaction trigger; undefined when compaction is disabled. */ compactionTrigger?: ThresholdCompactionTrigger; /** Compaction config (decision Q3); undefined when compaction is disabled. */ compactionConfig?: CompactionConfig; /** Active optional structural code intelligence provider. */ codeIndex?: CodeIntelligenceProvider; /** The authoritative `ExtensionHost` (single live capability surface). */ host: ExtensionHost; /** Typed skill registry snapshot (discovered Agent Skills). Empty when no resources. */ skills: SkillRegistrySnapshot; /** Layered context-file snapshot (SHARD.md / AGENTS.md). Empty when no resources. */ context: ContextFileSnapshot; /** Typed agent-definition snapshot (for later subagent use). Empty when no resources. */ agentDefinitions: AgentDefinitionSnapshot; /** Typed permission-rule snapshot (ordered first-match). Empty when no resources. */ permissionRules: PermissionRuleSnapshot; /** * Managed bootstrap context (saga u4uo9m.3). `undefined` for the direct/test path. * Carried as opaque data for later L3 extension injection (the bundled subagent tools * connect back to the daemon over `context.daemon_socket` and enforce * `context.launch_policy`/`context.depth`) without L2 importing L3. */ managed?: ManagedContext; /** Aggregated typed diagnostics across every specialized loader + the host. */ diagnostics: BootstrapDiagnostics; } /** * Fill the documented compaction defaults (saga dfcvo6): `enabled` → true, * `reserveTokens` → ~20% of `contextWindow`, `keepRecentTokens` → 8192. `contextWindow` * is required on the partial. Explicit values pass through unchanged. */ export function applyCompactionDefaults(partial: Partial): CompactionConfig { if (typeof partial.contextWindow !== 'number' || partial.contextWindow <= 0) { throw new Error( `shard: compaction.contextWindow must be a positive number (got ${JSON.stringify(partial.contextWindow)})`, ); } const contextWindow = partial.contextWindow; return { contextWindow, enabled: partial.enabled ?? true, reserveTokens: partial.reserveTokens ?? Math.round(contextWindow * 0.2), keepRecentTokens: partial.keepRecentTokens ?? 8192, }; } /** The fallback context window when neither a per-model nor global override is set. */ export { DEFAULT_CONTEXT_WINDOW }; // ── Trust-gate helpers ───────────────────────────────────────────────────────── /** A constant trust gate that always returns the injected `trusted` decision. */ function constantContextTrustGate(trusted: boolean): ContextTrustGate { return () => ({ trusted }); } function constantAgentTrustGate(trusted: boolean): AgentDefinitionTrustGate { return () => ({ trusted }); } function constantRuleTrustGate(trusted: boolean): PermissionRuleTrustGate { return () => ({ trusted }); } function constantExtensionTrustGate(trusted: boolean): ExtensionTrustGate { return () => ({ trusted }); } /** Identity used when the bootstrap registers the resolved provider through the host. */ const PROVIDER_IDENTITY: ExtensionIdentity = { id: 'shard.builtin.provider', name: 'OpenAI provider', version: '0.1.0', }; const SKILLS_IDENTITY: ExtensionIdentity = { id: 'shard.skills', name: 'Agent Skills', }; // ── Typed resource discovery ────────────────────────────────────────────────── /** Empty skill registry snapshot (used when discovery is skipped). */ const EMPTY_SKILL_SNAPSHOT: SkillRegistrySnapshot = { skills: [], diagnostics: [], catalog: [] }; /** * Resolve the EFFECTIVE project-boundary resolver for a resource-discovery config: * - an explicitly injected {@link BootstrapResourcesOptions.boundaryResolver} always wins; * - otherwise, on the PRODUCTION real-filesystem path (no fake `fs` seam injected), default * to the Git-backed {@link defaultProjectBoundaryResolver} so CLI/SDK and every canonical * caller share ONE canonical resolver across generic/specialized/skills/extensions walks; * - otherwise (a hermetic fake-fs test) return `undefined` so discovery stays on the cheap * filesystem `.git`-file-or-dir fallback and NEVER spawns Git. * * `stopAtGitRoot:false` still must not invoke/obey the resolver — that opt-out is enforced in * {@link resolveProjectAncestors}, which only consults the resolver when stopping is enabled. */ export function effectiveBoundaryResolver( resources: BootstrapResourcesOptions, ): ProjectBoundaryResolver | undefined { if (resources.boundaryResolver !== undefined) return resources.boundaryResolver; // `fs === undefined` is the "real disk" signal: a caller that injects a fake filesystem is // hermetic and must not spawn Git, so it stays on the filesystem marker fallback. return resources.fs === undefined ? defaultProjectBoundaryResolver() : undefined; } interface DiscoveredResources { skillRegistry: SkillRegistry; skills: SkillRegistrySnapshot; context: ContextFileSnapshot; agents: AgentDefinitionSnapshot; rules: PermissionRuleSnapshot; extensionDiagnostics: ReadonlyArray; } /** * Independently discover typed resources through the specialized loaders. Each loader owns * its format, trust, precedence, and diagnostics; results are NEVER flattened into a generic * `ResourceRecord`. The default filesystem is `nodeResourceFilesystem()`. Project files are * filtered unless `projectTrusted` is affirmed (safe default). Never throws — failures become * diagnostics. When `resources` is absent, returns empty snapshots (hermetic). */ async function discoverResources( resources: BootstrapResourcesOptions | undefined, defaultCwd: string, boundaryResolver: ProjectBoundaryResolver | undefined, ): Promise { if (resources === undefined) { return { skillRegistry: new DefaultSkillRegistry(), skills: EMPTY_SKILL_SNAPSHOT, context: { files: [], merged: '', diagnostics: [] }, agents: { agents: [], diagnostics: [] }, rules: { rules: [], records: [], diagnostics: [] }, extensionDiagnostics: [], }; } const fs = resources.fs ?? nodeResourceFilesystem(); const cwd = resources.cwd ?? defaultCwd; const userHome = resources.userHome; const bundledRoots = resources.bundledRoots ?? []; const userRoots = resources.userRoots ?? (userHome !== undefined ? [join(userHome, '.shard')] : []); const userExtensionRoots = resources.userExtensionRoots ?? (userHome !== undefined ? [join(userHome, '.shard')] : []); // Agent definitions are user-authored config (not runtime state): the dedicated agent // user layer prefers the XDG config dir. An explicit shared `userRoots` still wins for // backward compatibility; `userAgentRoots` overrides both. const agentUserRoots = resources.userAgentRoots ?? resources.userRoots ?? defaultAgentDefinitionUserRoots(userHome); const projectTrusted = resources.projectTrusted ?? false; const kinds = (resources.agentsMd ?? true) ? (['shard', 'agents'] as const) : (['shard'] as const); // Skill registry: inject one so load_skill can consult the live, populated instance. const skillRegistry = resources.skillRegistry ?? new DefaultSkillRegistry(); const skills = await loadSkillRegistry( { bundledRoots, ...(userHome !== undefined ? { userHome } : undefined), cwd, projectTrusted, registry: skillRegistry, ...(boundaryResolver !== undefined ? { boundaryResolver } : undefined), }, fs, ); const context = await new ContextFileLoader( { bundledRoots, userRoots, cwd, trustGate: constantContextTrustGate(projectTrusted), kinds, ...(boundaryResolver !== undefined ? { boundaryResolver } : undefined), }, fs, ).load(); const agents = await new AgentDefinitionLoader( { bundledRoots, userRoots: agentUserRoots, cwd, agentDirs: ['agents', '.shard/agents'], trustGate: constantAgentTrustGate(projectTrusted), includeBuiltins: true, ...(boundaryResolver !== undefined ? { boundaryResolver } : undefined), }, fs, ).load(); const rules = await new PermissionRuleLoader( { bundledRoots, userRoots, cwd, ruleDirs: ['rules', '.shard/rules'], trustGate: constantRuleTrustGate(projectTrusted), ...(boundaryResolver !== undefined ? { boundaryResolver } : undefined), }, fs, ).load(); // Dynamic extension discovery is code execution — the ExtensionLoader runs against the // host in {@link createDefaultCore}; here we only surface its (separately-collected) // diagnostics placeholder so the aggregated stream is complete. void userExtensionRoots; return { skillRegistry, skills, context, agents, rules, extensionDiagnostics: [], }; } /** * Compose the BASE system prompt in the saga-pinned order * (base identity → layered context → available-skills catalog). Empty sections are omitted. * `AgentSession` then appends active mode → durable activated-skill bodies → latest plan → * compaction handoff (see `agent-session.ts`). */ export function composeBaseSystemPrompt( baseIdentity: string, context: ContextFileSnapshot, skills: SkillRegistrySnapshot, ): string { const sections: string[] = [baseIdentity]; if (context.merged.length > 0) { sections.push(`\n${context.merged}`); } if (skills.catalog.length > 0) { const catalog = formatSkillCatalog(skills.catalog); if (catalog.length > 0) sections.push(catalog); } return sections.join('\n\n'); } /** * Construct the individual L2 pieces with the saga's defaults via the one async canonical * bootstrap (d4ks8c.7). Returns the wired pieces without bundling them into an * `AgentSession` — useful for tests or custom orchestration that wants to swap one piece. * * ## Permission rule composition precedence (req #4) * * For direct agents and managed roots, the gate consumes an ordered first-match `rules` list: * * 1. pinned `managed.rules` when present (highest authority), * 2. explicit `opts.permissions.rules`, * 3. discovered disk rules in the `PermissionRuleLoader`'s own first-match order * (project cwd → ancestors → user → bundled; untrusted project `allow` rules already * dropped by the loader). * * Managed children are isolated from host policy: their fresh child gate consumes only * pinned `managed.rules`. Within each group, source ordering is preserved. A * `RealPermissionGate` is constructed when any applicable rules or `opts.permissions` exist; * otherwise `AllowAllGate` is preserved for direct agents and managed roots. */ export async function createDefaultCore(opts: CreateAgentSessionOptions): Promise { const cwd = opts.cwd ?? process.cwd(); const agentId = opts.agentId ?? generateId(); // ── Provider triple (preserved verbatim) ─────────────────────────────────────── let model = opts.model; let apiKey = opts.apiKey; let auth = opts.auth; let baseURL = opts.baseURL; let api: 'chat-completions' | 'responses' = opts.api ?? 'chat-completions'; let reasoning = opts.reasoning === true; let reasoningEffort: string | undefined = opts.reasoningEffort; // saga p7m8jp.1: carry the supported effort IDs (gates chat-completions opt-in). let reasoningEfforts: string[] | undefined = opts.reasoningEfforts; if (model === undefined) { if (opts.resolveProvider === undefined) { throw new Error( 'shard: createDefaultCore needs either { model } (explicit) or { providerKey, modelName, resolveProvider } (config-driven); neither was supplied', ); } const resolved = opts.resolveProvider({ ...(opts.providerKey !== undefined ? { providerKey: opts.providerKey } : undefined), ...(opts.modelName !== undefined ? { modelName: opts.modelName } : undefined), }); model = resolved.model; apiKey = resolved.apiKey; auth = resolved.auth; baseURL = resolved.baseURL; if (resolved.api !== undefined) api = resolved.api; if (resolved.reasoning === true) reasoning = true; // saga p7m8jp.1: carry the resolved reasoning effort through the bootstrap path. if (resolved.reasoningEffort !== undefined) reasoningEffort = resolved.reasoningEffort; if (resolved.reasoningEfforts !== undefined) reasoningEfforts = resolved.reasoningEfforts; } const providerConfig: { model: string; auth?: 'bearer' | 'none'; apiKey?: string; baseURL?: string; modelDescriptor?: ModelDescriptor; } = { model }; if (auth !== undefined) providerConfig.auth = auth; if (apiKey !== undefined) providerConfig.apiKey = apiKey; if (baseURL !== undefined) providerConfig.baseURL = baseURL; if (opts.providerKey !== undefined) { providerConfig.modelDescriptor = { id: `${opts.providerKey}:model:${model}`, provider: opts.providerKey, model, }; } // saga hihsjg: pick the wire adapter. `responses` → the OpenAI Responses API provider // (readable reasoning summaries for gpt-5 / o-series); otherwise the OpenAI-compatible // chat-completions provider (llama.cpp / DeepSeek / vLLM / Ollama). // saga p7m8jp.1/p7m8jp.2: carry the resolved reasoning effort to the provider config. // For Responses, the effort always applies (defaulting to `medium` inside the provider). // For Chat Completions, the effort is sent ONLY when the model declared `reasoningEfforts` // (explicit opt-in); generic endpoints receive no new field by default. const reasoningEffortOpt = reasoningEffort !== undefined ? { reasoningEffort } : undefined; const chatReasoningEffortOpt = reasoningEfforts !== undefined && reasoningEffort !== undefined ? { reasoningEffort } : undefined; const provider: Provider = api === 'responses' ? new OpenAIResponsesProvider({ ...providerConfig, reasoning, ...reasoningEffortOpt, }) : new OpenAIProvider({ ...providerConfig, ...chatReasoningEffortOpt, }); // ── Typed resource discovery (independent; no ResourceRecord flattening) ─────── // Compute the effective boundary resolver ONCE so every loader (skills/context/agents/rules // + the dynamic extension loader below) shares one canonical resolver. Production real-fs // (no `fs` injected) defaults to the Git-backed resolver; hermetic fake-fs stays Git-free. const effectiveBoundary = opts.resources !== undefined ? effectiveBoundaryResolver(opts.resources) : undefined; const discovered = await discoverResources(opts.resources, cwd, effectiveBoundary); // saga u4uo9m.3: managed-child capability policy. When set, wrap the live SkillRegistry // with a filtered view BEFORE `load_skill` is installed (so the tool respects the // allowlist) and before the catalog is composed (so the system prompt hides filtered // skills). The base tool registry is filtered after the host snapshot (tools registered // by `opts.extensions` below are also subject to the policy). const managed = opts.managed; const capability = managed?.capability; const skillRegistry = capability !== undefined ? new FilteredSkillRegistry(discovered.skillRegistry, capability) : discovered.skillRegistry; // ── ExtensionHost: the exclusive live capability registration surface ─────────── const eventBus = new InProcessEventBus(); const host = new ExtensionHost({ eventBus, agentId }); // Bundled tool/mode/compactor extensions installed through host.install (req #3). const toolsExt = builtinToolsExtension(); await host.install(toolsExt.identity, toolsExt.install); const modesExt = builtinModesExtension(); await host.install(modesExt.identity, modesExt.install); const compactorExt = builtinCompactorExtension(); await host.install(compactorExt.identity, compactorExt.install); // SessionStore created before recall so the factory seam can capture it. const sessionStore = SessionStore.create(opts.sessionPath); const recallExt = createRecallExtension(sessionStore); await host.install(recallExt.identity, recallExt.install); // load_skill via ExtensionApi — only when skills exist (absence behavior; req #3/#9). // Uses the (possibly filtered) live skill registry so a filtered skill is invisible. if (skillRegistry.size > 0) { await host.install(SKILLS_IDENTITY, createLoadSkillExtension(skillRegistry, opts.resources?.fs ?? nodeResourceFilesystem())); } // Provider registered through ExtensionApi (keeps the host the single source of truth // while preserving backward-compatible `core.provider` typing). const providerInstall: ExtensionFunction = (api) => { api.registerProvider('default', provider as unknown as Provider); }; await host.install(PROVIDER_IDENTITY, providerInstall); // Trusted dynamic extensions via ExtensionLoader (req #3). Project sources require the // trust gate; default `projectTrusted` is `false` so no project code is executed. let extensionDiagnostics = discovered.extensionDiagnostics; if (opts.resources !== undefined) { const res = opts.resources; const userHome = res.userHome; const userExtRoots = res.userExtensionRoots ?? (userHome !== undefined ? [join(userHome, '.shard')] : []); const extLoader = new ExtensionLoader({ bundledRoots: res.bundledRoots ?? [], userRoots: userExtRoots, cwd: res.cwd ?? cwd, extensionDirs: ['extensions', '.shard/extensions'], ...(res.extensionTrustGate !== undefined ? { trustGate: res.extensionTrustGate } : { trustGate: constantExtensionTrustGate(res.projectTrusted ?? false) }), ...(res.extensionFs !== undefined ? { fs: res.extensionFs } : undefined), ...(res.importer !== undefined ? { importer: res.importer } : undefined), ...(effectiveBoundary !== undefined ? { boundaryResolver: effectiveBoundary } : undefined), }); await extLoader.load(host); extensionDiagnostics = [...discovered.extensionDiagnostics, ...extLoader.listDiagnostics()]; } // saga u4uo9m.3: generic injected bootstrap-contribution seam. Each caller-supplied // extension is installed through the ExtensionHost BEFORE the capability snapshot, so // its tools/modes/compactors are live and subject to the capability policy. L4 (the CLI) // builds these with the managed context captured in their closures — L2 never imports L3. if (opts.extensions !== undefined) { for (const ext of opts.extensions) { await host.install(ext.identity, ext.install); } } // saga u4uo9m.5 plan item 1: generic contribution FACTORY. Unlike `opts.extensions` (which // capture their own data the L4 built before this bootstrap ran), the factory receives the // trusted snapshots DISCOVERED by this bootstrap plus the pinned managed context — so the // bundled subagent tools get the trusted agent-definition snapshot for launch // prevalidation without the core importing `src/subagent/**`. The concrete L3 factory is // injected by the L4 CLI; the core only calls the L1 function type. Consulted whenever // `managed` is present, including a daemon-backed root context. if (opts.contributionFactory !== undefined && managed !== undefined) { const contributions = opts.contributionFactory({ snapshots: { agents: discovered.agents }, managed: managed.context, }); for (const ext of contributions) { await host.install(ext.identity, ext.install); } } // ── Build runtime registries from the host snapshot ONLY (req #3) ─────────────── const snapshot = host.snapshot(); const activeProvider = host.resolveProvider('default'); const codeIndex = host.activeCodeIntelligence(); if (activeProvider === undefined) { throw new Error('shard: no default provider registered through ExtensionApi'); } const modes = snapshot.modes; if (modes.length === 0) { throw new Error('shard: builtin modes extension registered no modes'); } const initialMode = modes.find((m) => m.name === 'build') ?? modes[0]!; const modeController = new ModeController(initialMode); for (const m of modes) { if (m.name !== initialMode.name) modeController.register(m); } const targetMode: DefaultMode = opts.mode ?? 'build'; if (modeController.select(targetMode) !== undefined) { modeController.setActive(targetMode); } const baseRegistry = new DefaultToolRegistry(modeController); for (const t of snapshot.tools) baseRegistry.register(t); // saga u4uo9m.3: strict capability filtering before provider/catalog exposure. An // explicit tool allowlist is validated against the registered tool names + the six // management names (deferred L3 registration is NOT a startup failure); unknown names // fail startup. The filtered registry applies the policy at exposure so tools registered // after bootstrap are still subject to it. Omitted allowlist → defaults, except a managed // child never exposes `subagent_launch`. let registry: DefaultToolRegistry | FilteredToolRegistry = baseRegistry; if (capability !== undefined && capability.tool_allowlist !== undefined) { const registered = new Set(snapshot.tools.map((t) => t.name)); const v = validateToolAllowlist(capability.tool_allowlist, registered); if (!v.ok) throw new Error(`shard: managed tool allowlist invalid — ${v.reason}`); } if (capability !== undefined && capability.skill_allowlist !== undefined) { const v = validateSkillAllowlist(capability.skill_allowlist, new Set(discovered.skills.skills.map((s) => s.name))); if (!v.ok) throw new Error(`shard: managed skill allowlist invalid — ${v.reason}`); } if (capability !== undefined) { registry = new FilteredToolRegistry(baseRegistry, capability); } // ── Permission gate: composed rules (req #4) ─────────────────────────────────── // saga u4uo9m.3: an actual managed child (`capability.is_child`) ALWAYS gets a fresh // RealPermissionGate with its own grant directory and a `read_only_allow` fallback // (rules/grants/mode decide first; otherwise effective read-only tools allow and uncovered // mutating tools ask). Managed roots retain normal root permission behavior. const discoveredRules = [...discovered.rules.rules]; const explicitRules = opts.permissions?.rules ?? []; const managedRules = managed?.rules ?? []; const grantsBaseDir = managed?.childGrantBaseDir ?? join(homedir(), '.shard', 'agents', agentId); let gate: PermissionGate; if (managed?.capability.is_child === true) { // Managed child: fresh gate, child grant dir, read-only-allow fallback. The isReadOnly // seam reads through the FILTERED registry so a hidden tool is treated as mutating // (safe: ask). Only manifest-pinned managed rules apply; host explicit/discovered rules // are intentionally excluded to preserve child isolation. Grant storage is lazy: // appendGrant creates the directory only when an allow-always decision is persisted. gate = new RealPermissionGate({ eventBus, modeController, agentId, baseDir: grantsBaseDir, defaultPolicy: 'read_only_allow', isReadOnly: (name: string) => registry.resolve(name)?.isReadOnly ?? false, ...(managedRules.length > 0 ? { rules: managedRules } : undefined), }); } else if (opts.permissions !== undefined || discoveredRules.length > 0 || managedRules.length > 0) { // Managed rules are pinned/authoritative, so they must precede direct and discovered rules. const combinedRules: Rule[] = [...managedRules, ...explicitRules, ...discoveredRules]; gate = new RealPermissionGate({ eventBus, modeController, agentId, baseDir: grantsBaseDir, ...(combinedRules.length > 0 ? { rules: combinedRules } : undefined), }); // Do not eagerly create ~/.shard/agents/. The grant store creates it on the // first persisted allow-always decision, avoiding one empty directory per runtime. } else { gate = new AllowAllGate(); } // ── Base system prompt: identity → layered context → available-skills catalog ── // saga u4uo9m.3: when managed, the caller (CLI) passes the pinned definition body as // `opts.systemPrompt` (the manifest's `system_prompt`), so the base identity is the // pinned body regardless of source-file edits (restart/relaunch determinism). The skill // catalog is the FILTERED snapshot when a capability policy is in effect. const skillsForPrompt = capability !== undefined ? skillRegistry.snapshot() : discovered.skills; const systemPrompt = composeBaseSystemPrompt( opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT, discovered.context, skillsForPrompt, ); // ── Compaction wiring (preserved; compactor now sourced from the host snapshot) ─ let compactor: Compactor | undefined; let compactionTrigger: ThresholdCompactionTrigger | undefined; let compactionConfig: CompactionConfig | undefined; if (opts.compaction) { compactor = snapshot.compactors[0]; if (compactor === undefined) { throw new Error('shard: compaction requested but no compactor registered via ExtensionApi'); } compactionTrigger = new ThresholdCompactionTrigger(); compactionConfig = applyCompactionDefaults(opts.compaction); } const diagnostics: BootstrapDiagnostics = { skills: discovered.skills.diagnostics, context: discovered.context.diagnostics, agents: discovered.agents.diagnostics, rules: discovered.rules.diagnostics, extensions: extensionDiagnostics, host: snapshot.diagnostics, }; return { provider: activeProvider, registry, gate, modeController, sessionStore, eventBus, agentId, ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}), cwd, systemPrompt, host, skills: skillsForPrompt, context: discovered.context, agentDefinitions: discovered.agents, permissionRules: discovered.rules, ...(managed !== undefined ? { managed: managed.context } : undefined), ...(codeIndex !== undefined ? { codeIndex } : undefined), diagnostics, ...(compactor !== undefined ? { compactor } : undefined), ...(compactionTrigger !== undefined ? { compactionTrigger } : undefined), ...(compactionConfig !== undefined ? { compactionConfig } : undefined), }; } /** * Construct an AgentSession from an already-bootstrapped core. This is the sole default * core→session adapter used by CLI, SDK, and createAgentSession, preventing call sites from * accidentally dropping compaction or future session dependencies. */ export function createAgentSessionFromCore(core: DefaultCore): AgentSession { return new AgentSessionClass({ provider: core.provider, registry: core.registry, gate: core.gate, modeController: core.modeController, sessionStore: core.sessionStore, eventBus: core.eventBus, agentId: core.agentId, ...(core.sessionId !== undefined ? { sessionId: core.sessionId } : {}), cwd: core.cwd, systemPrompt: core.systemPrompt, ...(core.codeIndex !== undefined ? { codeIndex: core.codeIndex } : undefined), ...(core.codeIndex !== undefined ? { onMutated: (paths: string[]) => { for (const path of paths) { try { core.codeIndex?.invalidate(path); } catch { // Best-effort: one failed path must not prevent later invalidations. } } }, } : undefined), ...(core.compactor !== undefined ? { compactor: core.compactor } : undefined), ...(core.compactionTrigger !== undefined ? { compactionTrigger: core.compactionTrigger } : undefined), ...(core.compactionConfig !== undefined ? { compactionConfig: core.compactionConfig } : undefined), }); } /** Construct a ready-to-run AgentSession through the canonical async bootstrap. */ export async function createAgentSession( opts: CreateAgentSessionOptions, ): Promise { return createAgentSessionFromCore(await createDefaultCore(opts)); }