@stackbone/sdk 0.1.0-alpha.3 → 0.1.0-alpha.4

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.
package/index.d.ts CHANGED
@@ -1,13 +1,15 @@
1
- import { ContractResponse } from '@stackbone/validators';
1
+ import { PublishJobResponse, ScheduleJobResponse, UnscheduleJobResponse, ListSchedulesResponse, ContractResponse } from '@stackbone/validators';
2
2
  import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
3
3
  import { Sql } from 'postgres';
4
+ import { IngestJobWriter, IngestRequest, IngestResponse, RagIngestProgress, DeleteOptions, DeleteResponse, RetrieveRequest, RetrieveHit, ChunkOptions, ParseInput, ParseOptions } from '@stackbone/rag-core';
5
+ export { ChunkOptions, ChunkStrategy, DeleteOptions, DeleteResponse, IngestChunk, IngestRequest, IngestRequestAutoEmbed, IngestRequestPrecomputed, IngestResponse, ParseInput, ParseOptions, RagIngestProgress, RetrieveHit, RetrieveRequest, RetrieveRequestAutoEmbed, RetrieveRequestPrecomputed } from '@stackbone/rag-core';
4
6
  import OpenAI from 'openai';
5
7
  import { ChatCompletionCreateParamsStreaming, ChatCompletionChunk, ChatCompletionCreateParamsNonStreaming, ChatCompletion, EmbeddingCreateParams, CreateEmbeddingResponse, ChatCompletionMessageParam } from 'openai/resources';
6
8
  import { Stream } from 'openai/streaming';
7
- import { RunStepsSpanProcessorOptions, RunStepsSpanProcessor, PlatformLoggerOptions, PlatformLogger, AggregateRunCostResult } from './observability/index.js';
8
9
  import { S3Client } from '@aws-sdk/client-s3';
9
10
  import { z } from 'zod';
10
11
  export { z } from 'zod';
12
+ import { SecretCipher } from '@stackbone/crypto';
11
13
 
12
14
  /**
13
15
  * Optional overrides accepted by `createClient`. All fields are optional;
@@ -29,11 +31,17 @@ interface ClientConfig {
29
31
  * every SDK consumer that talks to the agent's Postgres.
30
32
  */
31
33
  databaseUrl?: string;
34
+ /**
35
+ * Per-agent encryption key (base64, 32 bytes) used to decrypt the agent's
36
+ * own application secrets read directly from `stackbone_platform.secrets`.
37
+ * Falls back to `STACKBONE_SECRET_KEY` — the per-agent key the saga/harness
38
+ * inject into the runtime env. This is the ONLY new env channel the
39
+ * agent-owned-secrets split adds; the deliberate no-generic-env-channel
40
+ * stance is preserved.
41
+ */
42
+ secretKey?: string;
32
43
  openrouterKey?: string;
33
44
  openrouterBaseUrl?: string;
34
- qstashToken?: string;
35
- qstashCurrentSigningKey?: string;
36
- qstashNextSigningKey?: string;
37
45
  llamaParseApiKey?: string;
38
46
  /** mem0 API key for the long-term memory backend (`client.memory`). Falls back to `MEM0_API_KEY`. */
39
47
  mem0ApiKey?: string;
@@ -125,8 +133,14 @@ interface ResolvedConfig {
125
133
  readonly approvalSigningKey: string | undefined;
126
134
  /** Resolved from `config.agentId` ?? `STACKBONE_AGENT_ID`. */
127
135
  readonly agentId: string | undefined;
128
- /** Resolved from `config.databaseUrl` ?? `STACKBONE_POSTGRES_URL`. Shared by `client.database`, `client.rag` and `client.observability`. */
136
+ /** Resolved from `config.databaseUrl` ?? `STACKBONE_POSTGRES_URL`. Shared by `client.database`, `client.rag`, and the platform observability hooks in `@stackbone/sdk/observability`. */
129
137
  readonly databaseUrl: string | undefined;
138
+ /**
139
+ * Resolved from `config.secretKey` ?? `STACKBONE_SECRET_KEY`. The per-agent
140
+ * base64 encryption key `client.secrets` uses to decrypt rows read straight
141
+ * out of `stackbone_platform.secrets`.
142
+ */
143
+ readonly secretKey: string | undefined;
130
144
  /** Resolved from `config.openrouterKey` ?? `OPENROUTER_API_KEY`. */
131
145
  readonly openrouterKey: string | undefined;
132
146
  /** Resolved from `config.openrouterBaseUrl` ?? `OPENROUTER_BASE_URL`. `undefined` means use the OpenRouter default. */
@@ -223,22 +237,47 @@ declare const SDK_ERROR_CODE_PREFIXES: {
223
237
  */
224
238
  readonly rag: readonly ["dim_mismatch", "embedding_failed", "embedding_model_unsupported", "error", "ingest_cancelled", "invalid_request", "job_insert_failed", "jobs_error", "schema_missing"];
225
239
  /**
226
- * `client.approval` — control-plane facade plus the HMAC verify helper.
227
- * The transport-driven codes (`unauthorized`, `forbidden`, ...) come from
228
- * `HttpClient`'s status→domain remap with `prefix: 'approval'`; the
229
- * verify-specific codes (`invalid_signature`, etc.) are emitted locally.
240
+ * `client.approval` — AGENT-LOCAL DB facade plus the HMAC verify helper.
241
+ * The request/cancel/get/list codes (`persist_failed`, `cancel_failed`,
242
+ * `unavailable`) come from the direct write/read against
243
+ * `stackbone_platform.approvals`; the verify-specific codes
244
+ * (`invalid_signature`, etc.) are emitted locally.
230
245
  */
231
- readonly approval: readonly ["forbidden", "invalid_payload", "invalid_request", "invalid_response", "invalid_signature", "not_found", "rate_limited", "signature_expired", "signing_key_missing", "timeout", "tool_execute_failed", "unauthorized", "unavailable"];
246
+ readonly approval: readonly ["cancel_failed", "forbidden", "invalid_payload", "invalid_request", "invalid_response", "invalid_signature", "not_found", "persist_failed", "rate_limited", "signature_expired", "signing_key_missing", "timeout", "tool_execute_failed", "unauthorized", "unavailable"];
232
247
  /**
233
- * `client.secrets` — control-plane facade. Almost entirely transport-
234
- * driven (HttpClient remap with `prefix: 'secrets'`); the local codes are
235
- * the request-shape rail.
248
+ * `client.secrets` — AGENT-LOCAL facade. Reads `stackbone_platform.secrets`
249
+ * over the shared `client.database` pool and decrypts with the per-agent
250
+ * key. `not_configured` covers a missing `STACKBONE_SECRET_KEY`;
251
+ * `decrypt_failed` covers a key/envelope mismatch; `unavailable` covers a
252
+ * failed DB read.
236
253
  */
237
- readonly secrets: readonly ["already_exists", "forbidden", "invalid_request", "invalid_response", "not_found", "rate_limited", "timeout", "unauthorized", "unavailable"];
254
+ readonly secrets: readonly ["already_exists", "decrypt_failed", "forbidden", "invalid_request", "invalid_response", "not_configured", "not_found", "rate_limited", "timeout", "unauthorized", "unavailable"];
238
255
  /**
239
- * `client.config` — symmetric to `secrets`, points at `/api/config`.
256
+ * `client.config` — AGENT-LOCAL facade, reads the singleton
257
+ * `stackbone_platform.agent_config` row over the shared `client.database`
258
+ * pool.
240
259
  */
241
260
  readonly config: readonly ["forbidden", "invalid_request", "invalid_response", "not_found", "rate_limited", "timeout", "unauthorized", "unavailable"];
261
+ /**
262
+ * `client.queues` — the agent → control plane job-enqueue surface. Every
263
+ * method (`publish`, `schedule`, `unschedule`, `listSchedules`) POSTs/GETs
264
+ * the BullMQ dispatcher endpoints over `HttpClient`, so the full
265
+ * status→domain remap (`queues_unauthorized`, `queues_not_found`,
266
+ * `queues_unavailable`, …) is in play. The agent never touches Redis — it
267
+ * only calls these endpoints.
268
+ */
269
+ readonly queues: readonly ["forbidden", "invalid_request", "invalid_response", "not_found", "rate_limited", "timeout", "unauthorized", "unavailable"];
270
+ /**
271
+ * `client.prompts` — AGENT-LOCAL facade. Reads
272
+ * `stackbone_platform.prompts` / `prompt_versions` over the shared
273
+ * `client.database` pool and compiles templates with the local
274
+ * `@stackbone/prompt-compiler` (Mustache subset). `not_configured` covers a
275
+ * missing prompts schema (the `42P01` "run `stackbone db migrate up`" hint);
276
+ * `not_found` covers an absent / soft-deleted key; `missing_var` is the
277
+ * compile-time diagnostic when a `{{var}}` has no value; `unavailable`
278
+ * covers a failed DB read; `already_exists` covers a duplicate `create`.
279
+ */
280
+ readonly prompts: readonly ["already_exists", "invalid_request", "missing_var", "not_configured", "not_found", "unavailable"];
242
281
  /**
243
282
  * `client.memory` — reserved. The README documents the prefix; today the
244
283
  * pending surface returns `not_implemented` for every method so no
@@ -260,9 +299,10 @@ declare const SDK_ERROR_CODE_PREFIXES: {
260
299
  */
261
300
  readonly database: readonly ["not_configured"];
262
301
  /**
263
- * `client.observability`only the run-cost rollup emits a domain code
264
- * today; everything else either returns ok or surfaces through the
265
- * `database_*` family above.
302
+ * Observability — the run-cost rollup hook in `@stackbone/sdk/observability`
303
+ * (wired by the runtime, not a creator surface) emits this when its post-run
304
+ * Postgres write fails; everything else either returns ok or surfaces through
305
+ * the `database_*` family above.
266
306
  */
267
307
  readonly observability: readonly ["close_run_failed"];
268
308
  /**
@@ -467,6 +507,265 @@ declare class DatabaseModule {
467
507
  raw(): DrizzleClient;
468
508
  }
469
509
 
510
+ interface ApprovalToolSpec<I, O> {
511
+ name: string;
512
+ description: string;
513
+ parameters: Record<string, unknown>;
514
+ needsApproval?: boolean | ((input: I) => boolean | Promise<boolean>);
515
+ /**
516
+ * Maps the LLM input to the approval request fields. `topic` and `payload`
517
+ * default to the tool name and the raw input respectively when omitted;
518
+ * `onDecide` must always be supplied so the control plane knows where to
519
+ * deliver the eventual decision.
520
+ */
521
+ toRequest: (input: I) => Omit<ApprovalRequestOptions<I>, 'payload' | 'topic'> & Partial<Pick<ApprovalRequestOptions<I>, 'payload' | 'topic'>>;
522
+ execute: (input: I) => Promise<O> | O;
523
+ }
524
+ interface OpenAIToolSpec {
525
+ type: 'function';
526
+ function: {
527
+ name: string;
528
+ description: string;
529
+ parameters: Record<string, unknown>;
530
+ };
531
+ }
532
+ type ToolInvokeResult<O> = {
533
+ status: 'pending';
534
+ approvalId: string;
535
+ expiresAt: string;
536
+ } | {
537
+ status: 'ok';
538
+ result: O;
539
+ };
540
+ interface ApprovalTool<I, O> {
541
+ name: string;
542
+ description: string;
543
+ parameters: Record<string, unknown>;
544
+ openaiSpec(): OpenAIToolSpec;
545
+ invoke(input: I): Promise<Result<ToolInvokeResult<O>>>;
546
+ }
547
+
548
+ interface VerifyOptions {
549
+ /** Override the signing key resolved from `STACKBONE_APPROVAL_SIGNING_KEY` / `approvalSigningKey`. */
550
+ signingKey?: string;
551
+ /** Reject signatures whose timestamp is older than this (seconds). Default 300. */
552
+ toleranceSeconds?: number;
553
+ /** Inject a clock for tests. Defaults to `Date.now`. */
554
+ now?: () => number;
555
+ }
556
+
557
+ type ApprovalTopic = string;
558
+ declare const DECISION_STATUSES: readonly ["approved", "rejected", "timed_out", "cancelled"];
559
+ type DecisionStatus = (typeof DECISION_STATUSES)[number];
560
+ type ApprovalStatus = 'pending' | DecisionStatus;
561
+ type Decision<T = unknown> = {
562
+ status: 'approved' | 'rejected';
563
+ payload: T;
564
+ approver: ApproverInfo;
565
+ decidedAt: string;
566
+ reason?: string;
567
+ } | {
568
+ status: 'timed_out';
569
+ decidedAt: string;
570
+ } | {
571
+ status: 'cancelled';
572
+ decidedAt: string;
573
+ reason?: string;
574
+ };
575
+ interface ApproverInfo {
576
+ id: string;
577
+ email: string;
578
+ name?: string;
579
+ }
580
+ interface ApprovalRequestOptions<T = unknown> {
581
+ topic: ApprovalTopic;
582
+ payload: T;
583
+ title?: string;
584
+ description?: string;
585
+ /** Path on the agent the control plane will POST the decision to (resolved against the agent's public URL server-side). */
586
+ onDecide: string;
587
+ /** ISO 8601 duration (`'24h'`) or milliseconds. Default 24h. */
588
+ timeout?: string | number;
589
+ onTimeout?: 'reject' | 'approve' | 'ignore';
590
+ approver?: string;
591
+ /** Same `(topic, idempotencyKey)` returns the same `approvalId` instead of creating a new one. */
592
+ idempotencyKey?: string;
593
+ metadata?: Record<string, unknown>;
594
+ }
595
+ interface ApprovalRequest {
596
+ approvalId: string;
597
+ status: 'pending';
598
+ callbackUrl: string;
599
+ expiresAt: string;
600
+ }
601
+ interface ApprovalRecord<T = unknown> {
602
+ approvalId: string;
603
+ topic: ApprovalTopic;
604
+ status: ApprovalStatus;
605
+ payload: T;
606
+ decision?: Decision<T>;
607
+ createdAt: string;
608
+ expiresAt: string;
609
+ metadata?: Record<string, unknown>;
610
+ }
611
+ interface ApprovalListOptions {
612
+ status?: ApprovalStatus;
613
+ topic?: ApprovalTopic;
614
+ cursor?: string;
615
+ /** 1..100. Default 50. */
616
+ limit?: number;
617
+ }
618
+ interface ApprovalListResult<T = unknown> {
619
+ items: ApprovalRecord<T>[];
620
+ nextCursor?: string;
621
+ }
622
+ /**
623
+ * `client.approval` — AGENT-LOCAL. The HITL pause is recorded directly in the
624
+ * agent's own `stackbone_platform.approvals` table over the shared
625
+ * `client.database` pool; there is no control-plane POST anymore. The RESUME
626
+ * half stays Studio-driven: a human decides in Studio, the control plane signs
627
+ * an HMAC callback to `callbackUrl`, and the agent reconciles it with
628
+ * `verify()` (local crypto — see `verify.ts`).
629
+ *
630
+ * The write populates only the columns the cloud `create()` used to set
631
+ * (topic/payload/callback_url/idempotency_key/fallback/metadata/timeout_at;
632
+ * `schema` was sourced from a DTO field the SDK options object does not carry,
633
+ * so it stays NULL). `workspace_id`/`agent_id`/`run_id` stay NULL exactly as
634
+ * they were on the control-plane path — the idempotency `ON CONFLICT` relies on
635
+ * `workspace_id` being NULL with `NULLS NOT DISTINCT` (migration 0008).
636
+ */
637
+ declare class ApprovalFacade {
638
+ private readonly _resolved;
639
+ /** Lazy accessor for `client.database` — reaches the shared `Sql` via `.shared().$client`. */
640
+ private readonly _getDatabase;
641
+ constructor(_resolved: ResolvedConfig,
642
+ /** Lazy accessor for `client.database` — reaches the shared `Sql` via `.shared().$client`. */
643
+ _getDatabase: () => DatabaseModule);
644
+ request<T = unknown>(options: ApprovalRequestOptions<T>): Promise<Result<ApprovalRequest>>;
645
+ cancel(approvalId: string, reason?: string): Promise<Result<void>>;
646
+ get<T = unknown>(approvalId: string): Promise<Result<ApprovalRecord<T>>>;
647
+ list<T = unknown>(options?: ApprovalListOptions): Promise<Result<ApprovalListResult<T>>>;
648
+ /**
649
+ * Local crypto verification — does not touch the database, so it has the same
650
+ * shape it had on the control-plane surface. Auditing rule: a method gates
651
+ * iff it issues a datapath call; this one never does.
652
+ */
653
+ verify<T = unknown>(request: Request, options?: VerifyOptions): Promise<Result<Decision<T>>>;
654
+ /**
655
+ * Pure factory — returns an `ApprovalTool` whose `invoke()` ultimately calls
656
+ * back into `ApprovalFacade.request`.
657
+ */
658
+ tool<I, O>(spec: ApprovalToolSpec<I, O>): ApprovalTool<I, O>;
659
+ private sql;
660
+ }
661
+
662
+ declare class ConfigFacade {
663
+ private readonly _resolved;
664
+ /** Lazy accessor for `client.database` — see `SecretsFacade`. */
665
+ private readonly _getDatabase;
666
+ constructor(_resolved: ResolvedConfig,
667
+ /** Lazy accessor for `client.database` — see `SecretsFacade`. */
668
+ _getDatabase: () => DatabaseModule);
669
+ get<T = unknown>(key: string): Promise<Result<T>>;
670
+ /**
671
+ * Keys absent from the agent's config come back as omissions in the
672
+ * response — the returned map only contains entries the agent DB actually
673
+ * has, so callers must access fields with `?.` (the return type is
674
+ * `Partial<T>`).
675
+ */
676
+ getMany<T extends Record<string, unknown> = Record<string, unknown>>(keys: string[]): Promise<Result<Partial<T>>>;
677
+ private loadPayload;
678
+ private sql;
679
+ }
680
+
681
+ /**
682
+ * A prompt's current/active state. Key-based (workspace-unique). `template` is
683
+ * the current version's content; `version` is the live version number.
684
+ */
685
+ interface Prompt {
686
+ key: string;
687
+ name: string;
688
+ description: string | null;
689
+ /** The current version's content. */
690
+ template: string;
691
+ /** The live version number. The first `create` produces version `1`. */
692
+ version: number;
693
+ /** `{{var}}` names referenced by the current version. */
694
+ variables: readonly string[];
695
+ metadata: Record<string, unknown> | null;
696
+ /** ISO 8601 UTC timestamp. */
697
+ createdAt: string;
698
+ updatedAt: string;
699
+ }
700
+ interface GetPromptOptions {
701
+ /** Pin a specific version. Omitted -> current/latest. */
702
+ version?: number;
703
+ }
704
+ interface ListPromptsOptions {
705
+ /** 1..100. Default 50. */
706
+ limit?: number;
707
+ }
708
+ interface ListPromptsResult {
709
+ /** Current version of each (non-deleted) prompt. */
710
+ items: readonly Prompt[];
711
+ }
712
+ interface CreatePromptRequest {
713
+ key: string;
714
+ name: string;
715
+ /** The version-1 content. */
716
+ template: string;
717
+ description?: string;
718
+ metadata?: Record<string, unknown>;
719
+ }
720
+ interface UpdatePromptOptions {
721
+ /** New content. Appends a new immutable version. */
722
+ template?: string;
723
+ name?: string;
724
+ description?: string | null;
725
+ /** Replaces the prompt-head metadata. */
726
+ metadata?: Record<string, unknown>;
727
+ }
728
+ interface DeletePromptOptions {
729
+ /** Reserved for future per-version deletes; ignored today (soft-deletes the key). */
730
+ version?: number;
731
+ }
732
+ interface DeletePromptResult {
733
+ key: string;
734
+ /** Number of prompt heads soft-deleted (0 or 1). */
735
+ deleted: number;
736
+ }
737
+ interface CompilePromptResult {
738
+ /** The rendered string. */
739
+ output: string;
740
+ /** The version the compile ran against. */
741
+ version: number;
742
+ }
743
+ declare class PromptsFacade {
744
+ private readonly _resolved;
745
+ /**
746
+ * Lazy accessor for `client.database`. The facade pulls the shared
747
+ * `postgres-js` `Sql` via `getDatabase().shared().$client` — never
748
+ * reaching into a sibling module's implementation file.
749
+ */
750
+ private readonly _getDatabase;
751
+ constructor(_resolved: ResolvedConfig,
752
+ /**
753
+ * Lazy accessor for `client.database`. The facade pulls the shared
754
+ * `postgres-js` `Sql` via `getDatabase().shared().$client` — never
755
+ * reaching into a sibling module's implementation file.
756
+ */
757
+ _getDatabase: () => DatabaseModule);
758
+ get(key: string, options?: GetPromptOptions): Promise<Result<Prompt>>;
759
+ compile(key: string, variables: Record<string, unknown>, options?: GetPromptOptions): Promise<Result<CompilePromptResult>>;
760
+ list(options?: ListPromptsOptions): Promise<Result<ListPromptsResult>>;
761
+ create(request: CreatePromptRequest): Promise<Result<Prompt>>;
762
+ update(key: string, options: UpdatePromptOptions): Promise<Result<Prompt>>;
763
+ delete(key: string, _options?: DeletePromptOptions): Promise<Result<DeletePromptResult>>;
764
+ private sql;
765
+ private mapReadError;
766
+ private mapWriteError;
767
+ }
768
+
470
769
  /**
471
770
  * Public surface behind `client.ai`. Wraps the official `openai` SDK with a
472
771
  * `baseURL` override pointing to OpenRouter, so 300+ models are reachable
@@ -618,176 +917,17 @@ interface RequestOptions$1 {
618
917
  signal?: AbortSignal;
619
918
  }
620
919
 
621
- type ChunkStrategy = 'recursive' | 'sentence';
622
- interface ChunkOptions {
623
- /** Splitter algorithm. Default `recursive`. */
624
- strategy?: ChunkStrategy;
625
- /** Target characters per chunk. Default 512. */
626
- size?: number;
627
- /** Characters of overlap between consecutive chunks. Default 64. */
628
- overlap?: number;
629
- }
630
-
631
- type RagIngestProgress = {
632
- type: 'started';
633
- jobId: string;
634
- totalChunks: number;
635
- } | {
636
- type: 'progress';
637
- jobId: string;
638
- processedChunks: number;
639
- totalChunks: number;
640
- currentDocument?: {
641
- source: string;
642
- ordinal: number;
643
- };
644
- } | {
645
- type: 'completed';
646
- jobId: string;
647
- totalChunks: number;
648
- } | {
649
- type: 'failed';
650
- jobId: string;
651
- error: string;
652
- };
653
- interface IngestJobStartArgs {
654
- collection: string;
655
- source: string;
656
- totalChunks: number;
657
- }
658
- interface IngestJobProgressArgs {
659
- jobId: string;
660
- processedChunks: number;
661
- totalChunks: number;
662
- currentDocument?: {
663
- source: string;
664
- ordinal: number;
665
- };
666
- }
667
- interface IngestJobCompleteArgs {
668
- jobId: string;
669
- totalChunks: number;
670
- }
671
- interface IngestJobFailArgs {
920
+ /**
921
+ * Handle returned by `client.rag.ingestAsync`. The job id is allocated
922
+ * synchronously against `stackbone_rag_jobs` so the caller can track / cancel
923
+ * the work via the `/api/rag/jobs/:jobId/*` surface; `events` is a streaming
924
+ * channel of progress events (ADR §D9 shape); `result` settles when the
925
+ * pipeline finishes (or fails / is cancelled).
926
+ */
927
+ interface IngestAsyncHandle {
672
928
  jobId: string;
673
- error: string;
674
- }
675
- interface IngestJobWriter {
676
- /**
677
- * Allocates a `stackbone_rag_jobs` row in `running` state and returns its
678
- * id. Caller is expected to surface this id to the user before any
679
- * progress event fires so a `client.rag.ingestAsync` consumer can
680
- * immediately address the job (e.g. `POST /api/rag/jobs/:jobId/cancel`).
681
- */
682
- start(args: IngestJobStartArgs): Promise<Result<{
683
- jobId: string;
684
- }>>;
685
- /** Idempotent — last write wins on `progress` jsonb. */
686
- progress(args: IngestJobProgressArgs): Promise<void>;
687
- /** Marks the row terminal (status='succeeded'). */
688
- complete(args: IngestJobCompleteArgs): Promise<void>;
689
- /** Marks the row terminal (status='failed') and stores the error message. */
690
- fail(args: IngestJobFailArgs): Promise<void>;
691
- /**
692
- * Polled between chunk batches. Returning `true` makes the pipeline abort
693
- * with `rag_ingest_cancelled`, leaving the row in whatever terminal state
694
- * the writer transitions it to next (typically `cancelled`).
695
- */
696
- isCancelled(args: {
697
- jobId: string;
698
- }): Promise<boolean>;
699
- }
700
-
701
- interface ParseOptions {
702
- /** MIME type override. If omitted, the parser sniffs the input (Blob.type or PDF magic bytes). */
703
- mime?: string;
704
- }
705
- type ParseInput = Blob | string | Uint8Array | ArrayBuffer;
706
-
707
- interface IngestChunk {
708
- content: string;
709
- embedding: number[];
710
- }
711
- interface IngestRequestBase {
712
- /** Document identifier — re-ingesting with the same `id` replaces all its chunks atomically. */
713
- id: string;
714
- metadata?: Record<string, unknown>;
715
- /** Logical namespace for separation. Default `'default'`. */
716
- namespace?: string;
717
- /**
718
- * Canonical collection name used by the `stackbone_rag_jobs` writer (F07)
719
- * to bind the row to a `stackbone_rag_collections` parent. Optional for
720
- * back-compat with pre-F07 callers — when absent, no job row is written
721
- * and progress is delivered only via `onProgress` (if any).
722
- */
723
- collection?: string;
724
- /**
725
- * Optional sink for streaming progress events. Fired in addition to (and
726
- * before) the matching `IngestJobWriter` call so synchronous `ingest`
727
- * consumers can observe progress without paying the SQL writer cost.
728
- */
729
- onProgress?: (event: RagIngestProgress) => void | Promise<void>;
730
- }
731
- interface IngestRequestPrecomputed extends IngestRequestBase {
732
- chunks: IngestChunk[];
733
- }
734
- interface IngestRequestAutoEmbed extends IngestRequestBase {
735
- chunks: string[];
736
- /** Embedding model id. When set, the pipeline calls the configured Embedder. */
737
- model: string;
738
- /**
739
- * Items per embeddings request. Consumed by the facade when it builds the
740
- * `Embedder`; ignored by the pipeline itself (the embedder is fully
741
- * configured at construction time).
742
- */
743
- batchSize?: number;
744
- }
745
- type IngestRequest = IngestRequestPrecomputed | IngestRequestAutoEmbed;
746
- interface IngestResponse {
747
- id: string;
748
- chunks: number;
749
- }
750
- interface DeleteOptions {
751
- namespace?: string;
752
- }
753
- interface DeleteResponse {
754
- deleted: number;
755
- }
756
- interface RetrieveRequestBase {
757
- topK?: number;
758
- filter?: Record<string, unknown>;
759
- namespace?: string;
760
- includeContent?: boolean;
761
- includeMetadata?: boolean;
762
- }
763
- interface RetrieveRequestPrecomputed extends RetrieveRequestBase {
764
- embedding: number[];
765
- }
766
- interface RetrieveRequestAutoEmbed extends RetrieveRequestBase {
767
- text: string;
768
- /** Must match the model used at ingest time. */
769
- model: string;
770
- }
771
- type RetrieveRequest = RetrieveRequestPrecomputed | RetrieveRequestAutoEmbed;
772
- interface RetrieveHit {
773
- id: string;
774
- chunkIdx: number;
775
- content?: string;
776
- metadata?: Record<string, unknown>;
777
- score: number;
778
- }
779
-
780
- /**
781
- * Handle returned by `client.rag.ingestAsync`. The job id is allocated
782
- * synchronously against `stackbone_rag_jobs` so the caller can track / cancel
783
- * the work via the `/api/rag/jobs/:jobId/*` surface; `events` is a streaming
784
- * channel of progress events (ADR §D9 shape); `result` settles when the
785
- * pipeline finishes (or fails / is cancelled).
786
- */
787
- interface IngestAsyncHandle {
788
- jobId: string;
789
- events: AsyncIterable<RagIngestProgress>;
790
- result: Promise<Result<IngestResponse>>;
929
+ events: AsyncIterable<RagIngestProgress>;
930
+ result: Promise<Result<IngestResponse>>;
791
931
  }
792
932
  /**
793
933
  * `client.rag` — `pgvector`-backed retrieval. Two shapes per write/read:
@@ -807,9 +947,9 @@ interface IngestAsyncHandle {
807
947
  * consumer; memory and queues will adopt the same accessor when they
808
948
  * land.
809
949
  *
810
- * Schema readiness: starting with feature 30, the canonical schema is
811
- * installed by the CLI (`stackbone db migrate add-rag` + `migrate up`). When
812
- * an operation hits `42P01 relation does not exist`, the pipeline returns
950
+ * Schema readiness: the canonical schema (`stackbone_platform.rag_*`) is
951
+ * provisioned by the platform migrator, present on every install. When an
952
+ * operation still hits `42P01 relation does not exist`, the pipeline returns
813
953
  * `SdkError('rag_schema_missing')` with an actionable hint.
814
954
  */
815
955
  /**
@@ -884,10 +1024,11 @@ declare class RagModule {
884
1024
  deleteWhere(filter: Record<string, unknown>, options?: DeleteOptions): Promise<Result<DeleteResponse>>;
885
1025
  retrieve(request: RetrieveRequest): Promise<Result<RetrieveHit[]>>;
886
1026
  /**
887
- * Drops the legacy ad-hoc RAG schema. Kept for backwards compatibility with
888
- * pre-feature-30 agents whose tables were provisioned by `ensureSchema` on
889
- * first call. The canonical RAG schema (feature 30) is owned by the CLI and
890
- * removed via `stackbone db migrate` flows, not by this method.
1027
+ * Drops the legacy ad-hoc RAG tables (`rag_chunks` / `_rag_meta`) that
1028
+ * pre-feature-30 agents provisioned on first ingest. Kept for backwards
1029
+ * compatibility so an old agent can clean those up after upgrading. The
1030
+ * canonical RAG schema (`stackbone_platform.rag_*`) is owned by the platform
1031
+ * migrator — this method never touches it.
891
1032
  */
892
1033
  reset(): Promise<Result<void>>;
893
1034
  /**
@@ -908,365 +1049,41 @@ declare class RagModule {
908
1049
  * Future cross-module transaction propagation plugs a tx-bound `Sql`
909
1050
  * into the pipeline at the same call site, replacing the pool-bound
910
1051
  * one without touching the rest of RAG.
911
- *
912
- * Configuration errors (`STACKBONE_POSTGRES_URL` unset) flow up as a
913
- * `Result.err` with the same `database_not_configured` shape
914
- * `client.database` raises, instead of throwing through the public
915
- * surface.
916
- */
917
- private sql;
918
- private withPipeline;
919
- }
920
-
921
- /** Subset of `RequestInit['body']` we serialize without modification. */
922
- type SerializedBody = NonNullable<RequestInit['body']>;
923
- /**
924
- * Per-status overrides facades attach when an HTTP status carries a
925
- * non-canonical meaning for the endpoint — e.g. a `POST` whose `409` means
926
- * "already exists" instead of the default "conflict". Keys are HTTP statuses,
927
- * values are domain codes the transport should surface verbatim (no prefix
928
- * concatenation — the override is the final code).
929
- *
930
- * The value is typed against the catalog (`SdkErrorCode`) so an endpoint
931
- * cannot smuggle a wire code that isn't in the documented inventory.
932
- */
933
- type ErrorOverrides = Record<number, SdkErrorCode>;
934
- /**
935
- * Tells the transport how to remap HTTP status codes onto the surface's
936
- * domain code prefix. `404 → '<prefix>_not_found'`, `401 → '<prefix>_unauthorized'`,
937
- * etc. Endpoint-specific overrides win over the canonical mapping.
938
- *
939
- * `prefix` is constrained to `SdkErrorPrefix` (the catalog's prefix tuple) so
940
- * an unknown surface name doesn't compile — keeps the wire codes the
941
- * transport emits inside the typed inventory.
942
- */
943
- interface ErrorMapping {
944
- /** Prefix concatenated with `_not_found`, `_unauthorized`, …. Required for any domain remap. */
945
- prefix: SdkErrorPrefix;
946
- /** Per-status final code that overrides the canonical mapping. */
947
- overrides?: ErrorOverrides;
948
- }
949
- interface RequestOptions extends Omit<RequestInit, 'body' | 'signal'> {
950
- /** Scalar query params. `undefined` values are dropped, others get coerced via `String()`. */
951
- params?: Record<string, string | number | boolean | undefined>;
952
- /**
953
- * Repeated string-array params validated and serialised by the transport.
954
- * Empty arrays / blank elements reject upstream with `<prefix>_invalid_request`
955
- * (falls back to `http_invalid_request` when no `errorMapping` is set). Each
956
- * array is joined with `,` to match the canonical control-plane shape.
957
- */
958
- arrayParams?: Record<string, readonly string[]>;
959
- body?: SerializedBody | Record<string, unknown> | unknown[] | null;
960
- signal?: AbortSignal;
961
- /** Allow retrying non-idempotent requests (POST, PATCH). Off by default to prevent duplicate writes. */
962
- idempotent?: boolean;
963
- }
964
- /**
965
- * Shape every facade method ideally collapses into: "POST to /path with this
966
- * body, validate against this schema, surface errors as `<prefix>_*`". The
967
- * transport handles validation, HTTP→domain remapping, querystring assembly
968
- * and the `Result` envelope so facades stay tiny orchestrators.
969
- *
970
- * `arrayParams` keys default to `errorMapping.prefix` for the validation
971
- * error code (e.g. an empty `names: []` becomes `secrets_invalid_request`).
972
- */
973
- interface TransportRequest<T> extends RequestOptions {
974
- method: string;
975
- path: string;
976
- /**
977
- * Zod schema (or any object with a `.safeParse()` method) the parsed body
978
- * must validate against. On failure the transport surfaces
979
- * `<prefix>_invalid_response` (or `http_invalid_response` if no prefix is
980
- * configured) with the schema issues attached to `meta.issues`.
981
- */
982
- responseSchema?: ResponseSchema<T>;
983
- /** HTTP→domain remapping. Omit to let `http_*` codes flow through unchanged. */
984
- errorMapping?: ErrorMapping;
985
- }
986
- /**
987
- * Minimal contract the transport needs from a schema — `safeParse`. Compatible
988
- * with Zod (`z.object(…).safeParse(value)`) and any hand-rolled validator that
989
- * follows the same shape. Avoids leaking Zod's full surface into the transport
990
- * signature.
991
- */
992
- interface ResponseSchema<T> {
993
- safeParse(input: unknown): {
994
- success: true;
995
- data: T;
996
- } | {
997
- success: false;
998
- error: {
999
- issues: unknown;
1000
- };
1001
- };
1002
- }
1003
- interface HttpClientOptions {
1004
- /** Default 30_000. Set to 0 to disable. */
1005
- timeout?: number;
1006
- /** Default 3. Set to 0 to disable. */
1007
- retryCount?: number;
1008
- /** Initial backoff in ms; doubles each attempt with ±15% jitter. Default 500. */
1009
- retryDelay?: number;
1010
- /** Override the global fetch (useful for tests). */
1011
- fetch?: typeof fetch;
1012
- }
1013
- /**
1014
- * Shared HTTP client used by every facade that talks to the Stackbone
1015
- * control plane. Wraps `fetch` with timeout, exponential backoff with jitter,
1016
- * idempotent-only retries, response validation (Zod-compatible schemas) and
1017
- * a uniform `Result` envelope so facades never throw at the SDK boundary.
1018
- *
1019
- * Each facade tells the transport its error prefix (`'secrets'`, `'config'`,
1020
- * `'approval'`) so 404/401/403/429/5xx remap to surface-specific codes —
1021
- * `secrets_not_found`, `approval_unauthorized`, etc. Endpoint-specific
1022
- * remaps (e.g. `409 → 'secrets_already_exists'`) flow through
1023
- * `errorMapping.overrides`. Transport-level failures (network, timeout,
1024
- * parse) keep the `http_*` prefix the README contract documents.
1025
- *
1026
- * Reads `STACKBONE_API_URL` and `STACKBONE_AGENT_JWT` lazily on every request so
1027
- * env-var rotation is picked up without restarting the client.
1028
- */
1029
- declare class HttpClient {
1030
- private readonly resolved;
1031
- private readonly timeout;
1032
- private readonly retryCount;
1033
- private readonly retryDelay;
1034
- private readonly fetchImpl;
1035
- constructor(resolved: ResolvedConfig, options?: HttpClientOptions);
1036
- /**
1037
- * Convenience for `request({ method: 'GET', path, ... })`. Kept so legacy
1038
- * callers (tests, pending facades) keep compiling — the deep work happens
1039
- * in `request()`.
1040
- */
1041
- get<T>(path: string, options?: RequestOptions): Promise<Result<T>>;
1042
- post<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1043
- put<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1044
- patch<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1045
- delete<T>(path: string, options?: RequestOptions): Promise<Result<T>>;
1046
- /**
1047
- * The transport entry point every facade should target. Handles base-URL
1048
- * resolution, header injection, body/querystring serialisation, retry +
1049
- * timeout, response validation and HTTP→domain error remapping.
1050
- */
1051
- request<T>(req: TransportRequest<T>): Promise<Result<T>>;
1052
- private computeBackoff;
1053
- }
1054
-
1055
- interface ApprovalToolSpec<I, O> {
1056
- name: string;
1057
- description: string;
1058
- parameters: Record<string, unknown>;
1059
- needsApproval?: boolean | ((input: I) => boolean | Promise<boolean>);
1060
- /**
1061
- * Maps the LLM input to the approval request fields. `topic` and `payload`
1062
- * default to the tool name and the raw input respectively when omitted;
1063
- * `onDecide` must always be supplied so the control plane knows where to
1064
- * deliver the eventual decision.
1065
- */
1066
- toRequest: (input: I) => Omit<ApprovalRequestOptions<I>, 'payload' | 'topic'> & Partial<Pick<ApprovalRequestOptions<I>, 'payload' | 'topic'>>;
1067
- execute: (input: I) => Promise<O> | O;
1068
- }
1069
- interface OpenAIToolSpec {
1070
- type: 'function';
1071
- function: {
1072
- name: string;
1073
- description: string;
1074
- parameters: Record<string, unknown>;
1075
- };
1076
- }
1077
- type ToolInvokeResult<O> = {
1078
- status: 'pending';
1079
- approvalId: string;
1080
- expiresAt: string;
1081
- } | {
1082
- status: 'ok';
1083
- result: O;
1084
- };
1085
- interface ApprovalTool<I, O> {
1086
- name: string;
1087
- description: string;
1088
- parameters: Record<string, unknown>;
1089
- openaiSpec(): OpenAIToolSpec;
1090
- invoke(input: I): Promise<Result<ToolInvokeResult<O>>>;
1091
- }
1092
-
1093
- interface VerifyOptions {
1094
- /** Override the signing key resolved from `STACKBONE_APPROVAL_SIGNING_KEY` / `approvalSigningKey`. */
1095
- signingKey?: string;
1096
- /** Reject signatures whose timestamp is older than this (seconds). Default 300. */
1097
- toleranceSeconds?: number;
1098
- /** Inject a clock for tests. Defaults to `Date.now`. */
1099
- now?: () => number;
1100
- }
1101
-
1102
- type ApprovalTopic = string;
1103
- declare const DECISION_STATUSES: readonly ["approved", "rejected", "timed_out", "cancelled"];
1104
- type DecisionStatus = (typeof DECISION_STATUSES)[number];
1105
- type ApprovalStatus = 'pending' | DecisionStatus;
1106
- type Decision<T = unknown> = {
1107
- status: 'approved' | 'rejected';
1108
- payload: T;
1109
- approver: ApproverInfo;
1110
- decidedAt: string;
1111
- reason?: string;
1112
- } | {
1113
- status: 'timed_out';
1114
- decidedAt: string;
1115
- } | {
1116
- status: 'cancelled';
1117
- decidedAt: string;
1118
- reason?: string;
1119
- };
1120
- interface ApproverInfo {
1121
- id: string;
1122
- email: string;
1123
- name?: string;
1124
- }
1125
- interface ApprovalRequestOptions<T = unknown> {
1126
- topic: ApprovalTopic;
1127
- payload: T;
1128
- title?: string;
1129
- description?: string;
1130
- /** Path on the agent the control plane will POST the decision to (resolved against the agent's public URL server-side). */
1131
- onDecide: string;
1132
- /** ISO 8601 duration (`'24h'`) or milliseconds. Default 24h. */
1133
- timeout?: string | number;
1134
- onTimeout?: 'reject' | 'approve' | 'ignore';
1135
- approver?: string;
1136
- /** Same `(topic, idempotencyKey)` returns the same `approvalId` instead of creating a new one. */
1137
- idempotencyKey?: string;
1138
- metadata?: Record<string, unknown>;
1139
- }
1140
- interface ApprovalRequest {
1141
- approvalId: string;
1142
- status: 'pending';
1143
- callbackUrl: string;
1144
- expiresAt: string;
1145
- }
1146
- interface ApprovalRecord<T = unknown> {
1147
- approvalId: string;
1148
- topic: ApprovalTopic;
1149
- status: ApprovalStatus;
1150
- payload: T;
1151
- decision?: Decision<T>;
1152
- createdAt: string;
1153
- expiresAt: string;
1154
- metadata?: Record<string, unknown>;
1155
- }
1156
- interface ApprovalListOptions {
1157
- status?: ApprovalStatus;
1158
- topic?: ApprovalTopic;
1159
- cursor?: string;
1160
- /** 1..100. Default 50. */
1161
- limit?: number;
1162
- }
1163
- interface ApprovalListResult<T = unknown> {
1164
- items: ApprovalRecord<T>[];
1165
- nextCursor?: string;
1166
- }
1167
- declare class ApprovalFacade {
1168
- private readonly _resolved;
1169
- private readonly _http;
1170
- private readonly _gate;
1171
- constructor(_resolved: ResolvedConfig, _http: HttpClient,
1172
- /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
1173
- gate?: ModuleGate);
1174
- request<T = unknown>(options: ApprovalRequestOptions<T>): Promise<Result<ApprovalRequest>>;
1175
- cancel(approvalId: string, reason?: string): Promise<Result<void>>;
1176
- get<T = unknown>(approvalId: string): Promise<Result<ApprovalRecord<T>>>;
1177
- list<T = unknown>(options?: ApprovalListOptions): Promise<Result<ApprovalListResult<T>>>;
1178
- /**
1179
- * Local crypto verification — does not touch the datapath, so it is NOT
1180
- * gated by the contract handshake. Auditing rule: a method gates iff it
1181
- * issues an HTTP request to the configured baseUrl.
1182
- */
1183
- verify<T = unknown>(request: Request, options?: VerifyOptions): Promise<Result<Decision<T>>>;
1184
- /**
1185
- * Pure factory — returns an `ApprovalTool` whose `invoke()` ultimately
1186
- * calls back into `ApprovalFacade.request`, so the gate fires there. Not
1187
- * gated here.
1188
- */
1189
- tool<I, O>(spec: ApprovalToolSpec<I, O>): ApprovalTool<I, O>;
1190
- }
1191
-
1192
- declare class ConfigFacade {
1193
- private readonly _http;
1194
- private readonly _gate;
1195
- constructor(resolved: ResolvedConfig, _http: HttpClient,
1196
- /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
1197
- gate?: ModuleGate);
1198
- get<T = unknown>(key: string): Promise<Result<T>>;
1199
- /**
1200
- * Keys absent from the agent's config come back as omissions in the
1201
- * response — the returned map only contains entries the control plane
1202
- * actually has, so callers must access fields with `?.` (the return type
1203
- * is `Partial<T>`).
1204
- */
1205
- getMany<T extends Record<string, unknown> = Record<string, unknown>>(keys: string[]): Promise<Result<Partial<T>>>;
1206
- }
1207
-
1208
- declare class SecretsFacade {
1209
- private readonly _http;
1210
- private readonly _gate;
1211
- constructor(resolved: ResolvedConfig, _http: HttpClient,
1212
- /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
1213
- gate?: ModuleGate);
1214
- get(name: string): Promise<Result<string>>;
1215
- /**
1216
- * Names absent from the organization come back as omissions in the response —
1217
- * the returned map only contains entries the control plane actually has.
1218
- * Callers that need "all-or-nothing" semantics should diff the keys.
1052
+ *
1053
+ * Configuration errors (`STACKBONE_POSTGRES_URL` unset) flow up as a
1054
+ * `Result.err` with the same `database_not_configured` shape
1055
+ * `client.database` raises, instead of throwing through the public
1056
+ * surface.
1219
1057
  */
1220
- getMany(names: string[]): Promise<Result<Record<string, string>>>;
1058
+ private sql;
1059
+ private withPipeline;
1221
1060
  }
1222
1061
 
1223
- /**
1224
- * `client.observability` — span processor + per-run JSONL/OTLP logger +
1225
- * run-cost rollup. Local mode writes JSONL to `~/.stackbone/dev/runs/`; cloud
1226
- * mode forwards OTLP/HTTP to the collector.
1227
- *
1228
- * Contract gating: not gated. Local writes touch the filesystem (no
1229
- * Stackbone wire surface) and cloud writes go to OpenTelemetry, not the
1230
- * Stackbone Agent Protocol — so there is no `Capability` to assert against.
1231
- * No entry in `MODULE_CAPABILITIES` per the rule documented in
1232
- * `contract/capability-registry.ts`.
1233
- */
1234
- declare class ObservabilityModule {
1062
+ declare class SecretsFacade {
1235
1063
  private readonly _resolved;
1236
- private _processor?;
1237
- private readonly _loggers;
1238
- private _sql;
1239
- private _sqlResolved;
1240
- constructor(_resolved: ResolvedConfig);
1241
1064
  /**
1242
- * Singleton SpanProcessor that the agent registers in its OTel
1243
- * TracerProvider. Coexists with the OTLP exporter cloud builds wire
1244
- * both so Postgres feeds Studio (paridad con local) and Tempo feeds
1245
- * Platform Ops.
1065
+ * Lazy accessor for `client.database`. The facade pulls the shared
1066
+ * `postgres-js` `Sql` via `getDatabase().shared().$client`never
1067
+ * reaching into a sibling module's implementation file.
1246
1068
  */
1247
- spanProcessor(options?: RunStepsSpanProcessorOptions): RunStepsSpanProcessor;
1248
- flush(): Promise<Result<void>>;
1069
+ private readonly _getDatabase;
1070
+ private _cipher?;
1071
+ constructor(_resolved: ResolvedConfig,
1249
1072
  /**
1250
- * Per-run structured logger. In local mode (no `OTEL_EXPORTER_OTLP_ENDPOINT`)
1251
- * appends JSONL to `~/.stackbone/dev/runs/<runId>.jsonl`; in cloud mode batches
1252
- * OTLP/HTTP/JSON log records to the collector. Cached by `runId` so the
1253
- * file handle / buffered timer stays stable across repeated calls inside
1254
- * the same run.
1073
+ * Lazy accessor for `client.database`. The facade pulls the shared
1074
+ * `postgres-js` `Sql` via `getDatabase().shared().$client` never
1075
+ * reaching into a sibling module's implementation file.
1255
1076
  */
1256
- logger(options: {
1257
- runId: string;
1258
- } & Partial<PlatformLoggerOptions>): PlatformLogger;
1259
- /** Closes the per-run logger (flushes the JSONL file or the OTLP buffer). */
1260
- closeLogger(runId: string): Promise<void>;
1077
+ _getDatabase: () => DatabaseModule);
1078
+ get(name: string): Promise<Result<string>>;
1261
1079
  /**
1262
- * Hook the agent calls when a run finishes. Sums `cost_usd` from every
1263
- * `llm_call` span of the run and writes the total to
1264
- * `stackbone_platform.runs.cost_estimated_usd`. Errors are folded into the
1265
- * Result envelope so the agent's run cleanup never aborts on a flaky
1266
- * Postgres call — the cost is decorative metadata, not load-bearing.
1080
+ * Names absent from the agent come back as omissions in the response the
1081
+ * returned map only contains entries the agent DB actually has. Callers that
1082
+ * need "all-or-nothing" semantics should diff the keys.
1267
1083
  */
1268
- closeRun(runId: string): Promise<Result<AggregateRunCostResult>>;
1269
- private resolveSql;
1084
+ getMany(names: string[]): Promise<Result<Record<string, string>>>;
1085
+ private cipher;
1086
+ private sql;
1270
1087
  }
1271
1088
 
1272
1089
  interface StorageObject {
@@ -1363,30 +1180,6 @@ declare class StorageBucket {
1363
1180
  private resolve;
1364
1181
  }
1365
1182
 
1366
- /**
1367
- * OAuth connections (Notion, GDrive, Slack…).
1368
- *
1369
- * Contract gating: not gated. The OAuth surface targets third-party
1370
- * providers, not the Stackbone Agent Protocol, so there is no entry in
1371
- * `MODULE_CAPABILITIES` (see `contract/capability-registry.ts`). When the
1372
- * runtime lands and a Stackbone-mediated capability shows up (e.g. control
1373
- * plane OAuth broker), we add the row and wire the gate.
1374
- */
1375
- declare class ConnectionsFacade {
1376
- constructor(_resolved: ResolvedConfig, _http: HttpClient);
1377
- list(): Promise<Result<readonly never[]>>;
1378
- }
1379
-
1380
- /** Emit events to the organization event bus. */
1381
- declare class EventsFacade {
1382
- private readonly _http;
1383
- private readonly _gate;
1384
- constructor(resolved: ResolvedConfig, _http: HttpClient,
1385
- /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
1386
- gate?: ModuleGate);
1387
- emit(_name: string, _payload: unknown): Promise<Result<void>>;
1388
- }
1389
-
1390
1183
  /**
1391
1184
  * Where a memory lives. Default `'user'`. `'session'` is short-lived and
1392
1185
  * collapsed (or dropped) by `endSession()`; `'agent'` is shared across every
@@ -1505,96 +1298,201 @@ declare class MemoryModule {
1505
1298
  endSession(_sessionId: string, _options?: EndSessionOptions): Promise<Result<EndSessionResult>>;
1506
1299
  }
1507
1300
 
1301
+ /** Subset of `RequestInit['body']` we serialize without modification. */
1302
+ type SerializedBody = NonNullable<RequestInit['body']>;
1508
1303
  /**
1509
- * A prompt managed by the Stackbone control plane. Templates are plain strings
1510
- * with Mustache-style `{{var}}` placeholdersno conditionals or loops.
1304
+ * Per-status overrides facades attach when an HTTP status carries a
1305
+ * non-canonical meaning for the endpoint e.g. a `POST` whose `409` means
1306
+ * "already exists" instead of the default "conflict". Keys are HTTP statuses,
1307
+ * values are domain codes the transport should surface verbatim (no prefix
1308
+ * concatenation — the override is the final code).
1309
+ *
1310
+ * The value is typed against the catalog (`SdkErrorCode`) so an endpoint
1311
+ * cannot smuggle a wire code that isn't in the documented inventory.
1511
1312
  */
1512
- interface Prompt {
1513
- name: string;
1514
- template: string;
1515
- /** Monotonically increasing per `name`. The first `create` produces version `1`. */
1516
- version: number;
1517
- /** Variable names parsed from the template. */
1518
- variables?: readonly string[];
1519
- metadata?: Record<string, unknown>;
1520
- /** ISO 8601 UTC timestamp. */
1521
- createdAt: string;
1522
- updatedAt: string;
1523
- }
1524
- interface GetPromptOptions {
1525
- /** Pin a specific version. Omitted -> latest. */
1526
- version?: number;
1527
- }
1528
- interface ListPromptsOptions {
1529
- /** 1..100. Default 50. */
1530
- limit?: number;
1531
- cursor?: string;
1532
- }
1533
- interface ListPromptsResult {
1534
- /** Latest version of each prompt. */
1535
- items: readonly Prompt[];
1536
- nextCursor?: string;
1313
+ type ErrorOverrides = Record<number, SdkErrorCode>;
1314
+ /**
1315
+ * Tells the transport how to remap HTTP status codes onto the surface's
1316
+ * domain code prefix. `404 '<prefix>_not_found'`, `401 '<prefix>_unauthorized'`,
1317
+ * etc. Endpoint-specific overrides win over the canonical mapping.
1318
+ *
1319
+ * `prefix` is constrained to `SdkErrorPrefix` (the catalog's prefix tuple) so
1320
+ * an unknown surface name doesn't compile — keeps the wire codes the
1321
+ * transport emits inside the typed inventory.
1322
+ */
1323
+ interface ErrorMapping {
1324
+ /** Prefix concatenated with `_not_found`, `_unauthorized`, …. Required for any domain remap. */
1325
+ prefix: SdkErrorPrefix;
1326
+ /** Per-status final code that overrides the canonical mapping. */
1327
+ overrides?: ErrorOverrides;
1537
1328
  }
1538
- interface CreatePromptRequest {
1539
- name: string;
1540
- template: string;
1541
- metadata?: Record<string, unknown>;
1329
+ interface RequestOptions extends Omit<RequestInit, 'body' | 'signal'> {
1330
+ /** Scalar query params. `undefined` values are dropped, others get coerced via `String()`. */
1331
+ params?: Record<string, string | number | boolean | undefined>;
1332
+ /**
1333
+ * Repeated string-array params validated and serialised by the transport.
1334
+ * Empty arrays / blank elements reject upstream with `<prefix>_invalid_request`
1335
+ * (falls back to `http_invalid_request` when no `errorMapping` is set). Each
1336
+ * array is joined with `,` to match the canonical control-plane shape.
1337
+ */
1338
+ arrayParams?: Record<string, readonly string[]>;
1339
+ body?: SerializedBody | Record<string, unknown> | unknown[] | null;
1340
+ signal?: AbortSignal;
1341
+ /** Allow retrying non-idempotent requests (POST, PATCH). Off by default to prevent duplicate writes. */
1342
+ idempotent?: boolean;
1542
1343
  }
1543
- interface UpdatePromptOptions {
1544
- /** New template. Bumps `version` by one. */
1545
- template?: string;
1546
- /** Shallow-merged onto the existing metadata. */
1547
- metadata?: Record<string, unknown>;
1344
+ /**
1345
+ * Shape every facade method ideally collapses into: "POST to /path with this
1346
+ * body, validate against this schema, surface errors as `<prefix>_*`". The
1347
+ * transport handles validation, HTTP→domain remapping, querystring assembly
1348
+ * and the `Result` envelope so facades stay tiny orchestrators.
1349
+ *
1350
+ * `arrayParams` keys default to `errorMapping.prefix` for the validation
1351
+ * error code (e.g. an empty `names: []` becomes `secrets_invalid_request`).
1352
+ */
1353
+ interface TransportRequest<T> extends RequestOptions {
1354
+ method: string;
1355
+ path: string;
1356
+ /**
1357
+ * Zod schema (or any object with a `.safeParse()` method) the parsed body
1358
+ * must validate against. On failure the transport surfaces
1359
+ * `<prefix>_invalid_response` (or `http_invalid_response` if no prefix is
1360
+ * configured) with the schema issues attached to `meta.issues`.
1361
+ */
1362
+ responseSchema?: ResponseSchema<T>;
1363
+ /** HTTP→domain remapping. Omit to let `http_*` codes flow through unchanged. */
1364
+ errorMapping?: ErrorMapping;
1548
1365
  }
1549
- interface DeletePromptOptions {
1550
- /** Delete a specific version. Omitted -> delete every version under `name`. */
1551
- version?: number;
1366
+ /**
1367
+ * Minimal contract the transport needs from a schema `safeParse`. Compatible
1368
+ * with Zod (`z.object(…).safeParse(value)`) and any hand-rolled validator that
1369
+ * follows the same shape. Avoids leaking Zod's full surface into the transport
1370
+ * signature.
1371
+ */
1372
+ interface ResponseSchema<T> {
1373
+ safeParse(input: unknown): {
1374
+ success: true;
1375
+ data: T;
1376
+ } | {
1377
+ success: false;
1378
+ error: {
1379
+ issues: unknown;
1380
+ };
1381
+ };
1552
1382
  }
1553
- interface DeletePromptResult {
1554
- name: string;
1555
- /** Number of versions actually removed. */
1556
- deleted: number;
1383
+ interface HttpClientOptions {
1384
+ /** Default 30_000. Set to 0 to disable. */
1385
+ timeout?: number;
1386
+ /** Default 3. Set to 0 to disable. */
1387
+ retryCount?: number;
1388
+ /** Initial backoff in ms; doubles each attempt with ±15% jitter. Default 500. */
1389
+ retryDelay?: number;
1390
+ /** Override the global fetch (useful for tests). */
1391
+ fetch?: typeof fetch;
1557
1392
  }
1558
1393
  /**
1559
- * `client.prompts` managed prompts for the agent. Prompts live outside the
1560
- * code so creators can edit, version and A/B test them without rebuilding the
1561
- * container. The first iteration is a scaffolding placeholder: every method
1562
- * returns `not_implemented`. The real backend will be the Stackbone control
1563
- * plane and the public surface defined here is the contract callers can
1564
- * already type against.
1394
+ * Shared HTTP client used by every facade that talks to the Stackbone
1395
+ * control plane. Wraps `fetch` with timeout, exponential backoff with jitter,
1396
+ * idempotent-only retries, response validation (Zod-compatible schemas) and
1397
+ * a uniform `Result` envelope so facades never throw at the SDK boundary.
1398
+ *
1399
+ * Each facade tells the transport its error prefix (`'secrets'`, `'config'`,
1400
+ * `'approval'`) so 404/401/403/429/5xx remap to surface-specific codes —
1401
+ * `secrets_not_found`, `approval_unauthorized`, etc. Endpoint-specific
1402
+ * remaps (e.g. `409 → 'secrets_already_exists'`) flow through
1403
+ * `errorMapping.overrides`. Transport-level failures (network, timeout,
1404
+ * parse) keep the `http_*` prefix the README contract documents.
1565
1405
  *
1566
- * Contract gating: not gated yet. Prompts will eventually talk to the
1567
- * Stackbone control plane, but no `Capability` has been declared for it
1568
- * (no entry in `MODULE_CAPABILITIES` — see `contract/capability-registry.ts`).
1569
- * When the runtime lands we add the row (e.g. `prompts.read_write`) and
1570
- * wire the gate the same way `secrets` / `config` do.
1406
+ * Reads `STACKBONE_API_URL` and `STACKBONE_AGENT_JWT` lazily on every request so
1407
+ * env-var rotation is picked up without restarting the client.
1571
1408
  */
1572
- declare class PromptsFacade {
1573
- constructor(_resolved: ResolvedConfig, _http: HttpClient);
1574
- get(_name: string, _options?: GetPromptOptions): Promise<Result<Prompt>>;
1575
- compile(_name: string, _variables: Record<string, unknown>, _options?: GetPromptOptions): Promise<Result<string>>;
1576
- list(_options?: ListPromptsOptions): Promise<Result<ListPromptsResult>>;
1577
- create(_request: CreatePromptRequest): Promise<Result<Prompt>>;
1578
- update(_name: string, _options: UpdatePromptOptions): Promise<Result<Prompt>>;
1579
- delete(_name: string, _options?: DeletePromptOptions): Promise<Result<DeletePromptResult>>;
1409
+ declare class HttpClient {
1410
+ private readonly resolved;
1411
+ private readonly timeout;
1412
+ private readonly retryCount;
1413
+ private readonly retryDelay;
1414
+ private readonly fetchImpl;
1415
+ constructor(resolved: ResolvedConfig, options?: HttpClientOptions);
1416
+ /**
1417
+ * Convenience for `request({ method: 'GET', path, ... })`. Kept so legacy
1418
+ * callers (tests, pending facades) keep compiling — the deep work happens
1419
+ * in `request()`.
1420
+ */
1421
+ get<T>(path: string, options?: RequestOptions): Promise<Result<T>>;
1422
+ post<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1423
+ put<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1424
+ patch<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1425
+ delete<T>(path: string, options?: RequestOptions): Promise<Result<T>>;
1426
+ /**
1427
+ * The transport entry point every facade should target. Handles base-URL
1428
+ * resolution, header injection, body/querystring serialisation, retry +
1429
+ * timeout, response validation and HTTP→domain error remapping.
1430
+ */
1431
+ request<T>(req: TransportRequest<T>): Promise<Result<T>>;
1432
+ private computeBackoff;
1580
1433
  }
1581
1434
 
1435
+ /**
1436
+ * Enqueue a one-shot (or deferred) job. The opaque `payload` is whatever the
1437
+ * agent's `receive` handler expects — the core never inspects it.
1438
+ */
1582
1439
  interface PublishRequest {
1583
- url: string;
1584
- body: unknown;
1440
+ /** Opaque job name (e.g. `send-email`). Surfaced in the Studio inspector. */
1441
+ name: string;
1442
+ /** Opaque job payload — JSON-serialisable; the core forwards it untouched. */
1443
+ payload: unknown;
1444
+ /** Per-job override of the platform retry default. Omitted → platform default. */
1585
1445
  retries?: number;
1446
+ /** Deferred delivery in milliseconds. Omitted / 0 → immediate. */
1586
1447
  delay?: number;
1448
+ /** Idempotency hint — a duplicate publish with the same key collapses to one job. */
1587
1449
  deduplicationId?: string;
1588
- headers?: Record<string, string>;
1589
1450
  }
1451
+ /** Register / update a dynamic per-installation cron schedule. */
1452
+ interface ScheduleRequest {
1453
+ /** Schedule name, unique per install. Re-scheduling the same name updates it in place. */
1454
+ name: string;
1455
+ /** Opaque payload delivered on every fire. */
1456
+ payload: unknown;
1457
+ /** Cron expression evaluated by the dispatcher. */
1458
+ cron: string;
1459
+ /** IANA timezone the cron pattern runs in. Defaults to UTC. */
1460
+ tz?: string;
1461
+ /** Per-job override of the platform retry default. */
1462
+ retries?: number;
1463
+ }
1464
+ /** Cancel a previously registered schedule by name. */
1465
+ interface UnscheduleRequest {
1466
+ name: string;
1467
+ }
1468
+ /**
1469
+ * `client.queues` — the agent's handle on the BullMQ-backed job system. The
1470
+ * agent never touches Redis: each method makes an authenticated call to the
1471
+ * control plane (the dispatcher owns the queue + the clock).
1472
+ *
1473
+ * - `publish` enqueues a one-shot job (optionally deferred via `delay`, with a
1474
+ * per-job `retries` override and an optional `deduplicationId`).
1475
+ * - `schedule` / `unschedule` manage dynamic per-user cron schedules.
1476
+ * - `listSchedules` lists the install's active schedules.
1477
+ *
1478
+ * Authentication reuses the existing `STACKBONE_AGENT_JWT` channel injected at
1479
+ * provisioning (the `HttpClient` attaches the bearer + install header), so the
1480
+ * core scopes every enqueue to the agent's own install.
1481
+ *
1482
+ * Contract gating: every method awaits the `queues.jobs` module gate before
1483
+ * touching the network, so a stale datapath / missing capability surfaces as
1484
+ * a gating error rather than a failed POST.
1485
+ */
1590
1486
  declare class QueuesModule {
1487
+ private readonly _http;
1591
1488
  private readonly _gate;
1592
- constructor(resolved: ResolvedConfig,
1489
+ constructor(resolved: ResolvedConfig, _http: HttpClient,
1593
1490
  /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
1594
1491
  gate?: ModuleGate);
1595
- publish(_request: PublishRequest): Promise<Result<{
1596
- messageId: string;
1597
- }>>;
1492
+ publish(request: PublishRequest): Promise<Result<PublishJobResponse>>;
1493
+ schedule(request: ScheduleRequest): Promise<Result<ScheduleJobResponse>>;
1494
+ unschedule(request: UnscheduleRequest): Promise<Result<UnscheduleJobResponse>>;
1495
+ listSchedules(): Promise<Result<ListSchedulesResponse>>;
1598
1496
  }
1599
1497
 
1600
1498
  /**
@@ -1603,18 +1501,25 @@ declare class QueuesModule {
1603
1501
  * groups each surface by where its state lives:
1604
1502
  *
1605
1503
  * - `surfaces/agent-local/` — Postgres branch owned by the agent
1606
- * (`database`, `rag`).
1504
+ * (`database`, `rag`, `secrets`, `config`, `approval`). `secrets`,
1505
+ * `config` and `approval` moved here from the control plane: the agent now
1506
+ * reads its secrets/config and records HITL pauses directly against its own
1507
+ * `stackbone_platform.*` tables (decrypting secrets with the per-agent key),
1508
+ * with no control-plane round-trip. The approval RESUME callback stays
1509
+ * Studio-signed and is reconciled locally by `approval.verify()`.
1607
1510
  * - `surfaces/external/` — managed partner SDKs the SDK wraps directly
1608
- * (`ai` / OpenRouter, `storage` / S3 + R2 / MinIO, `observability` /
1609
- * OpenTelemetry).
1511
+ * (`ai` / OpenRouter, `storage` / S3 + R2 / MinIO). Observability is not a
1512
+ * creator surface: agent code observes runs through the handler-scoped
1513
+ * `ctx.logger` (and plain `console.*`, which the wrapper captures and
1514
+ * correlates), while the platform primitives live in the
1515
+ * `@stackbone/sdk/observability` subpath the runtime wires directly.
1610
1516
  * - `surfaces/control-plane/` — thin HTTP calls to Stackbone's control
1611
- * plane (`approval`, `secrets`, `config`).
1517
+ * plane (none today after secrets/config/approval moved agent-local).
1612
1518
  * - `surfaces/pending/` — public surface is stable but the runtime is
1613
1519
  * not built yet. They are wired as live `client.X` accessors so MVP
1614
1520
  * agent code can be authored against the eventual contract today; each
1615
1521
  * method returns `Result<{ error: { code: "not_implemented" } }>` until
1616
- * the backing runtime ships. Today: queues, memory, prompts,
1617
- * connections, events.
1522
+ * the backing runtime ships. Today: queues, memory.
1618
1523
  *
1619
1524
  * Each accessor builds its surface on first access and caches it — env
1620
1525
  * vars and partner SDK init costs are paid only when the agent actually
@@ -1632,15 +1537,12 @@ declare class StackboneClient {
1632
1537
  private _storage?;
1633
1538
  private _ai?;
1634
1539
  private _rag?;
1635
- private _observability?;
1636
1540
  private _approval?;
1637
1541
  private _secrets?;
1638
1542
  private _config?;
1639
1543
  private _queues?;
1640
1544
  private _memory?;
1641
- private _events?;
1642
1545
  private _prompts?;
1643
- private _connections?;
1644
1546
  private _httpClient?;
1645
1547
  private readonly _contractStore;
1646
1548
  constructor(resolved: ResolvedConfig);
@@ -1649,16 +1551,16 @@ declare class StackboneClient {
1649
1551
  get storage(): StorageModule;
1650
1552
  get ai(): AiModule;
1651
1553
  get rag(): RagModule;
1652
- get observability(): ObservabilityModule;
1653
1554
  get approval(): ApprovalFacade;
1654
1555
  get secrets(): SecretsFacade;
1655
1556
  get config(): ConfigFacade;
1656
1557
  /**
1657
- * Pending surface — runtime not built. Every method returns
1658
- * `not_implemented` so MVP agent code can wire against the eventual
1659
- * contract today and switch over the day the backing runtime ships.
1660
- * Gated against `queues.pgmq` so the handshake-blocked / capability-
1661
- * missing paths are exercised the same way as the live surfaces.
1558
+ * Live surface — the agent's handle on the BullMQ-backed job system.
1559
+ * `publish` / `schedule` / `unschedule` / `listSchedules` make authenticated
1560
+ * calls to the control-plane dispatcher (`/api/v1/agent/queues/*`); the
1561
+ * agent never touches Redis. Gated against `queues.jobs` so the
1562
+ * handshake-blocked / capability-missing paths are exercised the same way
1563
+ * as the other gated surfaces.
1662
1564
  */
1663
1565
  get queues(): QueuesModule;
1664
1566
  /**
@@ -1668,23 +1570,14 @@ declare class StackboneClient {
1668
1570
  */
1669
1571
  get memory(): MemoryModule;
1670
1572
  /**
1671
- * Pending surface — runtime not built. Every method returns
1672
- * `not_implemented`. Gated against `events.bus` so handshake errors
1673
- * surface symmetrically with the other control-plane facades.
1674
- */
1675
- get events(): EventsFacade;
1676
- /**
1677
- * Pending surface — runtime not built. Every method returns
1678
- * `not_implemented`. Will gate once a capability row is declared in
1679
- * `MODULE_CAPABILITIES`; until then it is intentionally ungated.
1573
+ * Live agent-local surface — reads prompts from `stackbone_platform.prompts`
1574
+ * in the agent DB over the shared `client.database` pool and compiles
1575
+ * templates with the local `@stackbone/prompt-compiler`. Constructed with
1576
+ * the same `(resolved, () => this.database)` shape as `secrets`/`config`;
1577
+ * like them it is NOT wired to the control-plane handshake gate (it talks to
1578
+ * the agent's own tables, not a control-plane datapath).
1680
1579
  */
1681
1580
  get prompts(): PromptsFacade;
1682
- /**
1683
- * Pending surface — runtime not built. Returns an empty list today.
1684
- * Connections target third-party OAuth providers, not the Stackbone
1685
- * Agent Protocol, so this surface is intentionally not gated.
1686
- */
1687
- get connections(): ConnectionsFacade;
1688
1581
  /**
1689
1582
  * Read-only view of the most recently resolved Stackbone Agent Protocol
1690
1583
  * contract for this client's `stackboneApiUrl`. Returns `null` until a
@@ -1781,13 +1674,13 @@ interface InvokeContext<I extends z.ZodType> {
1781
1674
  logger: Logger;
1782
1675
  }
1783
1676
  /**
1784
- * The single `invoke` capability the creator exposes. Mirrors the wrapper's
1785
- * three HTTP verbs (`/invoke`, `/health`, `/schema`) F1 lands only the
1786
- * invoke half; `/health` and `/schema` land in later slices.
1677
+ * The synchronous `invoke` capability. Mirrors the wrapper's three HTTP verbs
1678
+ * (`/invoke`, `/health`, `/schema`): `invoke` is the only request path, its
1679
+ * `output` is the `/invoke` response body, and its schemas drive `/schema`.
1787
1680
  *
1788
1681
  * The fields are deliberately enumerated (no `[k: string]: unknown` escape
1789
- * hatch) so the parent `AgentSpec` can reject unknown keys at the type level
1790
- * via `keyof` checks downstream.
1682
+ * hatch) so the precise type the creator gets from `defineAgent` rejects
1683
+ * unknown keys on the block at compile time.
1791
1684
  */
1792
1685
  interface InvokeCapability<I extends z.ZodType, O extends z.ZodType> {
1793
1686
  /** Zod schema for the parsed `/invoke` request body. */
@@ -1796,47 +1689,232 @@ interface InvokeCapability<I extends z.ZodType, O extends z.ZodType> {
1796
1689
  output: O;
1797
1690
  /** Creator-supplied handler. Returns either the parsed output or a promise of it. */
1798
1691
  run: (ctx: InvokeContext<I>) => z.infer<O> | Promise<z.infer<O>>;
1799
- /** Optional per-invocation timeout in milliseconds. Wired by a later slice. */
1692
+ /** Optional per-invocation timeout in milliseconds. */
1693
+ timeoutMs?: number;
1694
+ }
1695
+ /**
1696
+ * A named **job handler** — a sibling of `invoke` at the top level of the spec,
1697
+ * reached asynchronously via `client.queues.publish({ name })` (the BullMQ
1698
+ * dispatcher pushes the job back to `/invoke` with that name; the wrapper routes
1699
+ * it here instead of to `invoke.run`).
1700
+ *
1701
+ * Same shape as `invoke` with one difference: `output` is **optional**. A job is
1702
+ * fire-and-forget — the dispatcher only checks the 2xx, the return value is
1703
+ * journalled, not delivered to a caller. Declaring `output` self-validates the
1704
+ * job's result; omitting it lets `run` return anything (or nothing).
1705
+ */
1706
+ interface JobCapability<I extends z.ZodType, O extends z.ZodType = z.ZodType> {
1707
+ /** Zod schema for the job payload (`publish`'s `payload`, revalidated here). */
1708
+ input: I;
1709
+ /** Optional Zod schema for the job's return value. Validated + journalled, never returned. */
1710
+ output?: O;
1711
+ /** Creator-supplied job handler. May return nothing — the result is not delivered. */
1712
+ run: (ctx: InvokeContext<I>) => z.infer<O> | void | Promise<z.infer<O> | void>;
1713
+ /** Optional per-job timeout in milliseconds. */
1800
1714
  timeoutMs?: number;
1801
1715
  }
1802
1716
  /**
1803
- * The declarative shape every Stackbone agent returns from `src/index.ts`.
1717
+ * Loose, non-generic view of a single capability (invoke or a job handler).
1718
+ * The wrapper / `loadSpec` read the spec through this — the precise per-handler
1719
+ * typing lives on the value `defineAgent` returns to the creator, not here.
1804
1720
  *
1805
- * `AgentSpec` is a closed object: the keys are listed explicitly so adding
1806
- * `{ invoke, anyOtherKey }` at the call site fails `tsc`. The corresponding
1807
- * runtime guard in `defineAgent` enforces the same invariant against callers
1808
- * that bypass the type system through `as any`.
1721
+ * `run` is a method signature (not an arrow property) on purpose: it makes the
1722
+ * parameter bivariant so every concrete `InvokeCapability` / `JobCapability`
1723
+ * (whose `run` is typed against its own narrow input) is assignable to it, which
1724
+ * is what lets `AgentSpec` carry a string index signature of capabilities.
1725
+ */
1726
+ interface AnyCapability {
1727
+ input: z.ZodType;
1728
+ output?: z.ZodType | undefined;
1729
+ run(ctx: InvokeContext<z.ZodType>): unknown;
1730
+ timeoutMs?: number | undefined;
1731
+ }
1732
+ /**
1733
+ * The declarative shape every Stackbone agent returns from `src/index.ts`, as
1734
+ * the wrapper / `loadSpec` consume it: the **loose runtime view**. Every
1735
+ * capability — `invoke` and each named job handler — is an `AnyCapability`
1736
+ * (`output` optional, `run` loose). `invoke` is required; the string index
1737
+ * carries the job handlers, reached by name through the BullMQ dispatcher.
1809
1738
  *
1810
- * `health` and other capabilities will join `invoke` as they land — every
1811
- * addition is an explicit, reviewable edit to this type.
1739
+ * The precise per-handler typing (each `ctx.input` inferred, `invoke.output`
1740
+ * required) lives on the value `defineAgent` returns, which is assignable to
1741
+ * this. At runtime `invoke.output` is always present — the spec seed forces it —
1742
+ * so the wrapper reads it behind a small guard.
1812
1743
  */
1813
- interface AgentSpec<I extends z.ZodType, O extends z.ZodType> {
1814
- invoke: InvokeCapability<I, O>;
1744
+ interface AgentSpec {
1745
+ /** The synchronous entry: the `/invoke` response + the `GET /schema` source. */
1746
+ invoke: AnyCapability;
1747
+ /** Named job handlers, reached via `client.queues.publish({ name })`. */
1748
+ [name: string]: AnyCapability;
1815
1749
  }
1816
1750
  /**
1817
- * Hard ceiling on `invoke.timeoutMs`. The platform saga kills the Machine
1818
- * before the wrapper would time out at higher values, so a creator declaring
1819
- * a 10-minute timeout would publish a contract the runtime cannot keep.
1820
- * Surface that conflict at boot instead of as a mysterious mid-invocation
1751
+ * Top-level keys the platform owns. A job handler may not use these names.
1752
+ * Only `invoke` for now; the contract adds `health` / `schema` / `events` /
1753
+ * `schedules` here if/when they become declarable capabilities (see the named
1754
+ * job handlers ADR).
1755
+ */
1756
+ declare const RESERVED_HANDLER_NAMES: readonly ["invoke"];
1757
+ /**
1758
+ * Hard ceiling on any capability's `timeoutMs`. The platform saga kills the
1759
+ * Machine before the wrapper would time out at higher values, so a creator
1760
+ * declaring a 10-minute timeout would publish a contract the runtime cannot
1761
+ * keep. Surface that conflict at boot instead of as a mysterious mid-invocation
1821
1762
  * SIGKILL in production.
1822
1763
  */
1823
1764
  declare const INVOKE_TIMEOUT_HARD_CAP_MS = 300000;
1824
1765
  /**
1825
- * Identity function with two jobs:
1766
+ * One handler block, generic **directly** on its input Zod schema `I`. `input: I`
1767
+ * is a single-level type parameter (not a nested indexed access like
1768
+ * `S['input']`), which is the shape TypeScript can invert when reading the spec
1769
+ * literal back — so every handler's `ctx.input` is inferred from its own schema.
1770
+ * `output` stays loose (`z.ZodType`) and `run`'s return is `unknown`: a handler's
1771
+ * result is validated against `output` at runtime (the wrapper), not at the type
1772
+ * layer, so output inference is not needed for the contract to hold.
1773
+ */
1774
+ type HandlerBlock<I extends z.ZodType> = {
1775
+ input: I;
1776
+ output?: z.ZodType;
1777
+ run: (ctx: InvokeContext<I>) => unknown | Promise<unknown>;
1778
+ timeoutMs?: number;
1779
+ };
1780
+ /**
1781
+ * Per-key precise shape of the spec the creator writes. `T` maps each handler
1782
+ * name to its **input** schema; `[K in keyof T]: HandlerBlock<T[K]>` is a
1783
+ * homomorphic mapped type whose value (`input: T[K]`) is a single-level access,
1784
+ * so TypeScript infers `T` back from the literal and keeps the concrete key
1785
+ * names. The `& { invoke: ... }` arm makes `invoke` required and forces its
1786
+ * `output` to be present (a job's `output` is optional).
1787
+ */
1788
+ type AgentSpecParam<T extends Record<string, z.ZodType>> = {
1789
+ [K in keyof T]: HandlerBlock<T[K]>;
1790
+ } & {
1791
+ invoke: {
1792
+ input: z.ZodType;
1793
+ output: z.ZodType;
1794
+ };
1795
+ };
1796
+ /**
1797
+ * Anchor + guard for `defineAgent`:
1826
1798
  *
1827
- * 1. **Inference anchor** — pins the input/output Zod schemas so creators get
1828
- * a fully typed `ctx.input` and a typed return value inside `run` without
1829
- * repeating themselves.
1830
- * 2. **Boot-time invariant** — throws on any unknown top-level or invoke-block
1831
- * key. This is the safety net for creators that force a shape through
1832
- * `as any`; the type system already rejects this, but the wrapper PRD's
1833
- * "impossible to break the contract from creator code" success criterion
1834
- * requires the runtime check too.
1799
+ * 1. **Inference anchor** — each handler (`invoke` and every named job) infers
1800
+ * its own `ctx.input` from its `input` schema. The creator never repeats a
1801
+ * type.
1802
+ * 2. **Boot-time invariant** — `invoke` is required; every other top-level key
1803
+ * must be a valid handler object reached via `client.queues.publish({ name })`.
1804
+ * The TypeScript signature already shapes this; the runtime guard is the
1805
+ * safety net for creators forcing a shape through `as any`.
1835
1806
  *
1836
1807
  * Returns the spec verbatim so callers can `export default defineAgent({...})`
1837
1808
  * and the wrapper can read the same object back through dynamic import.
1838
1809
  */
1839
- declare const defineAgent: <I extends z.ZodType, O extends z.ZodType>(spec: AgentSpec<I, O>) => AgentSpec<I, O>;
1810
+ declare function defineAgent<T extends Record<string, z.ZodType>>(spec: AgentSpecParam<T>): {
1811
+ [K in keyof T]: HandlerBlock<T[K]>;
1812
+ };
1813
+
1814
+ interface InvocationContext {
1815
+ /** The envelope's `invocationId` — surfaced on log lines as `trace_id`. */
1816
+ readonly invocationId: string;
1817
+ /** The run this invoke belongs to — surfaced on log lines as `run_id`. */
1818
+ readonly runId: string;
1819
+ /**
1820
+ * The handler this invoke routed to — `invoke` for a direct entry or the
1821
+ * named job handler (e.g. `demo-log-job`) the dispatcher delivered to.
1822
+ * Surfaced on log lines as `handler` so the Studio logs trail can tell which
1823
+ * entrypoint emitted each line. Optional so a caller that binds a context
1824
+ * without routing info (older tests, lifecycle hooks) still type-checks.
1825
+ */
1826
+ readonly handler?: string;
1827
+ }
1828
+ /**
1829
+ * Runs `fn` with `context` bound as the active invocation. The context stays
1830
+ * available to every synchronous and asynchronous continuation rooted in this
1831
+ * call (i.e. across `await`s inside `fn`). Returns whatever `fn` returns.
1832
+ */
1833
+ declare function runWithInvocationContext<T>(context: InvocationContext, fn: () => T): T;
1834
+ /**
1835
+ * Returns the active invocation context, or `undefined` when called outside any
1836
+ * invoke (process boot, lifecycle events, tests that don't set one up).
1837
+ */
1838
+ declare function getInvocationContext(): InvocationContext | undefined;
1839
+
1840
+ /** The subset of the global `console` this bridge replaces. */
1841
+ interface ConsoleLike {
1842
+ log: (...args: unknown[]) => void;
1843
+ info: (...args: unknown[]) => void;
1844
+ warn: (...args: unknown[]) => void;
1845
+ error: (...args: unknown[]) => void;
1846
+ debug: (...args: unknown[]) => void;
1847
+ }
1848
+ interface InstallConsoleCaptureOptions {
1849
+ /** Console object to patch. Defaults to the global `console`. Test seam. */
1850
+ console?: ConsoleLike;
1851
+ /** Sink for `log` / `info` / `debug`. Defaults to `process.stdout`. Test seam. */
1852
+ stdout?: LogSink;
1853
+ /** Sink for `warn` / `error`. Defaults to `process.stderr`. Test seam. */
1854
+ stderr?: LogSink;
1855
+ /** Clock for the `ts` field. Defaults to `() => new Date()`. Test seam. */
1856
+ now?: () => Date;
1857
+ }
1858
+ /**
1859
+ * Replaces `console.log/info/debug/warn/error` so that, while an invocation
1860
+ * context is active, each call is re-emitted as a structured log line carrying
1861
+ * `run_id` / `trace_id`. Returns a function that restores the originals.
1862
+ * Calling it twice on the same console is a no-op (returns a no-op restore).
1863
+ */
1864
+ declare function installInvocationConsoleCapture(options?: InstallConsoleCaptureOptions): () => void;
1865
+
1866
+ declare const STORED_TO_SDK_ENV: Readonly<Record<string, string>>;
1867
+ /** Encrypted envelope row as stored in `stackbone_platform.secrets`. */
1868
+ interface SystemSecretRow {
1869
+ name: string;
1870
+ version: string;
1871
+ nonce: Buffer | Uint8Array;
1872
+ ciphertext: Buffer | Uint8Array;
1873
+ }
1874
+ /**
1875
+ * Maps the `is_system` rows to the SDK env names, decrypting ONLY the rows that
1876
+ * have a known mapping, then derives two env-only channels. Existing values are
1877
+ * never overwritten (an explicit container/Machine override always wins). The
1878
+ * SDK env names this call populated are returned (sorted) so the caller can log
1879
+ * an observable, value-free "what got rehydrated" line — values are never
1880
+ * logged. Shared by both the cloud neon reader and the postgres-js reader so
1881
+ * they cannot drift on the mapping.
1882
+ *
1883
+ * Decryption is lazy on purpose: an unmapped stored row is skipped WITHOUT
1884
+ * decrypting it, so a non-envelope row (e.g. a probe row a stubbed query
1885
+ * returns) never throws. The two derived channels:
1886
+ * - `STACKBONE_POSTGRES_URL` from `databaseUrl` (the SDK's `client.database`
1887
+ * reads this name; the cloud harness's own Neon driver keeps `DATABASE_URL`).
1888
+ * - `STACKBONE_APPROVAL_SIGNING_KEY` from `HMAC_SECRET` so the SDK's approval
1889
+ * callback verifier reconciles with the signer (which signs with the same
1890
+ * boot-bundle HMAC).
1891
+ */
1892
+ declare function rehydrateSystemSecretsRows(env: NodeJS.ProcessEnv, rows: readonly SystemSecretRow[], cipher: SecretCipher, databaseUrl: string): string[];
1893
+ /**
1894
+ * Reads the `is_system` rows from `stackbone_platform.secrets`. Injectable so
1895
+ * tests run against a stub without a real Postgres; production opens a
1896
+ * short-lived `postgres-js` connection (see `loadSystemSecretsIntoEnv`).
1897
+ */
1898
+ type SystemSecretsReader = () => Promise<SystemSecretRow[]>;
1899
+ interface LoadSystemSecretsArgs {
1900
+ /** Connection string for the agent's Postgres (`STACKBONE_POSTGRES_URL`). */
1901
+ readonly databaseUrl: string;
1902
+ /** Per-agent base64 key the rows are decrypted with (`STACKBONE_SECRET_KEY`). */
1903
+ readonly secretKey: string;
1904
+ /** Target env to populate. Defaults to `process.env`. */
1905
+ readonly env?: NodeJS.ProcessEnv;
1906
+ /** Override the row reader (tests inject a stub). */
1907
+ readonly reader?: SystemSecretsReader;
1908
+ }
1909
+ /**
1910
+ * Reads every `is_system` row from the agent's Postgres via `postgres-js`,
1911
+ * decrypts each with the per-agent key, and rehydrates them into `env` under
1912
+ * the SDK's expected names. Used by the `stackbone dev` emulator and the
1913
+ * self-host runtime entry to give the agent the same credentials the cloud
1914
+ * harness rehydrates — without depending on a Neon HTTP endpoint, which local
1915
+ * dev does not have.
1916
+ */
1917
+ declare function loadSystemSecretsIntoEnv(args: LoadSystemSecretsArgs): Promise<string[]>;
1840
1918
 
1841
1919
  /**
1842
1920
  * Reserved error codes. Order is intentional: keep them flat (string) so
@@ -1968,4 +2046,4 @@ interface JsonSchemaDocument {
1968
2046
  */
1969
2047
  declare const analyzeAgentSchemas: (pair: AgentSchemaPair) => SchemaIntrospectionResult;
1970
2048
 
1971
- export { type AddMemoryRequest, type AgentSchemaPair, type AgentSpec, type ApprovalListOptions, type ApprovalListResult, type ApprovalRecord, type ApprovalRequest, type ApprovalRequestOptions, type ApprovalStatus, type ApprovalTool, type ApprovalToolSpec, type ApprovalTopic, type ApproverInfo, type ChunkOptions, type ChunkStrategy, type ClientConfig, type CreatePromptRequest, type Decision, type DecisionStatus, type DeleteAllMemoryRequest, type DeleteOptions, type DeletePromptOptions, type DeletePromptResult, type DeleteResponse, type EndSessionOptions, type EndSessionResult, type FatalConstruct, type GeneratedImage, type GetPromptOptions, INVOCATION_ID_HEADER, INVOKE_TIMEOUT_HARD_CAP_MS, type ImageGenerateParams, type ImagesResponse, type IngestAsyncHandle, type IngestChunk, type IngestRequest, type IngestRequestAutoEmbed, type IngestRequestPrecomputed, type IngestResponse, type InvokeCapability, type InvokeContext, type InvokeEnvelopeError, type InvokeErrorEnvelope, type InvokeRequest, type InvokeResponse, type InvokeSuccessEnvelope, type JsonSchemaDocument, type ListMemoryRequest, type ListMemoryResult, type ListOptions, type ListPromptsOptions, type ListPromptsResult, type LogSink, type Logger, type LoggerBindings, type MemoryContent, type MemoryHistoryEntry, type MemoryHistoryEvent, type MemoryHit, type MemoryItem, type MemoryScope, type ModelsListResponse, type OpenRouterModel, type ParseInput, type ParseOptions, type Prompt, type PublishRequest, RESERVED_ERROR_CODES, RUN_ID_HEADER, type RagIngestProgress, type ReservedErrorCode, type Result, type RetrieveHit, type RetrieveRequest, type RetrieveRequestAutoEmbed, type RetrieveRequestPrecomputed, type SchemaDiagnostic, type SchemaHalf, type SchemaIntrospectionResult, type SdkError, type SdkErrorCode, type SearchMemoryOptions, type SignedUrl, type SignedUrlOptions, StackboneClient, type StorageBody, type StorageObject, type StructuredLoggerOptions, type UpdateMemoryOptions, type UpdatePromptOptions, type UploadOptions, type VerifyOptions, type WarnConstruct, analyzeAgentSchemas, createClient, createStructuredLogger, defineAgent, invokeRequestSchema, isReservedErrorCode, isSdkErrorCode };
2049
+ export { type AddMemoryRequest, type AgentSchemaPair, type AgentSpec, type AnyCapability, type ApprovalListOptions, type ApprovalListResult, type ApprovalRecord, type ApprovalRequest, type ApprovalRequestOptions, type ApprovalStatus, type ApprovalTool, type ApprovalToolSpec, type ApprovalTopic, type ApproverInfo, type ClientConfig, type CompilePromptResult, type ConsoleLike, type CreatePromptRequest, type Decision, type DecisionStatus, type DeleteAllMemoryRequest, type DeletePromptOptions, type DeletePromptResult, type EndSessionOptions, type EndSessionResult, type FatalConstruct, type GeneratedImage, type GetPromptOptions, INVOCATION_ID_HEADER, INVOKE_TIMEOUT_HARD_CAP_MS, type ImageGenerateParams, type ImagesResponse, type IngestAsyncHandle, type InstallConsoleCaptureOptions, type InvocationContext, type InvokeCapability, type InvokeContext, type InvokeEnvelopeError, type InvokeErrorEnvelope, type InvokeRequest, type InvokeResponse, type InvokeSuccessEnvelope, type JobCapability, type JsonSchemaDocument, type ListMemoryRequest, type ListMemoryResult, type ListOptions, type ListPromptsOptions, type ListPromptsResult, type LoadSystemSecretsArgs, type LogSink, type Logger, type LoggerBindings, type MemoryContent, type MemoryHistoryEntry, type MemoryHistoryEvent, type MemoryHit, type MemoryItem, type MemoryScope, type ModelsListResponse, type OpenRouterModel, type Prompt, type PublishRequest, RESERVED_ERROR_CODES, RESERVED_HANDLER_NAMES, RUN_ID_HEADER, type ReservedErrorCode, type Result, STORED_TO_SDK_ENV, type ScheduleRequest, type SchemaDiagnostic, type SchemaHalf, type SchemaIntrospectionResult, type SdkError, type SdkErrorCode, type SearchMemoryOptions, type SignedUrl, type SignedUrlOptions, StackboneClient, type StorageBody, type StorageObject, type StructuredLoggerOptions, type SystemSecretRow, type SystemSecretsReader, type UnscheduleRequest, type UpdateMemoryOptions, type UpdatePromptOptions, type UploadOptions, type VerifyOptions, type WarnConstruct, analyzeAgentSchemas, createClient, createStructuredLogger, defineAgent, getInvocationContext, installInvocationConsoleCapture, invokeRequestSchema, isReservedErrorCode, isSdkErrorCode, loadSystemSecretsIntoEnv, rehydrateSystemSecretsRows, runWithInvocationContext };