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

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 { AgentConnectionListResponse, InvokeConnectorActionResponse, 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,59 @@ 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.connections` — the agent → control plane connectors proxy. Both
272
+ * methods (`list`, `invoke`) hit the `/api/v1/agent/connections/*` endpoints
273
+ * over `HttpClient`, so the full status→domain remap (`connections_unauthorized`,
274
+ * `connections_not_found`, `connections_unavailable`, …) is in play. The agent
275
+ * never holds a credential — it only calls these endpoints; the control plane
276
+ * resolves the workspace connection and runs the connector action. `ambiguous`
277
+ * is the 409 the proxy returns when the workspace holds several connections of
278
+ * the same connector and `invoke` carried no `{ connection }` selector to pick
279
+ * one — the error message lists the candidate names/ids so the author can.
280
+ */
281
+ readonly connections: readonly ["ambiguous", "forbidden", "invalid_request", "invalid_response", "not_found", "rate_limited", "timeout", "unauthorized", "unavailable"];
282
+ /**
283
+ * `client.prompts` — AGENT-LOCAL facade. Reads
284
+ * `stackbone_platform.prompts` / `prompt_versions` over the shared
285
+ * `client.database` pool and compiles templates with the local
286
+ * `@stackbone/prompt-compiler` (Mustache subset). `not_configured` covers a
287
+ * missing prompts schema (the `42P01` "run `stackbone db migrate up`" hint);
288
+ * `not_found` covers an absent / soft-deleted key; `missing_var` is the
289
+ * compile-time diagnostic when a `{{var}}` has no value; `unavailable`
290
+ * covers a failed DB read; `already_exists` covers a duplicate `create`.
291
+ */
292
+ readonly prompts: readonly ["already_exists", "invalid_request", "missing_var", "not_configured", "not_found", "unavailable"];
242
293
  /**
243
294
  * `client.memory` — reserved. The README documents the prefix; today the
244
295
  * pending surface returns `not_implemented` for every method so no
@@ -260,9 +311,10 @@ declare const SDK_ERROR_CODE_PREFIXES: {
260
311
  */
261
312
  readonly database: readonly ["not_configured"];
262
313
  /**
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.
314
+ * Observability — the run-cost rollup hook in `@stackbone/sdk/observability`
315
+ * (wired by the runtime, not a creator surface) emits this when its post-run
316
+ * Postgres write fails; everything else either returns ok or surfaces through
317
+ * the `database_*` family above.
266
318
  */
267
319
  readonly observability: readonly ["close_run_failed"];
268
320
  /**
@@ -467,6 +519,265 @@ declare class DatabaseModule {
467
519
  raw(): DrizzleClient;
468
520
  }
469
521
 
522
+ interface ApprovalToolSpec<I, O> {
523
+ name: string;
524
+ description: string;
525
+ parameters: Record<string, unknown>;
526
+ needsApproval?: boolean | ((input: I) => boolean | Promise<boolean>);
527
+ /**
528
+ * Maps the LLM input to the approval request fields. `topic` and `payload`
529
+ * default to the tool name and the raw input respectively when omitted;
530
+ * `onDecide` must always be supplied so the control plane knows where to
531
+ * deliver the eventual decision.
532
+ */
533
+ toRequest: (input: I) => Omit<ApprovalRequestOptions<I>, 'payload' | 'topic'> & Partial<Pick<ApprovalRequestOptions<I>, 'payload' | 'topic'>>;
534
+ execute: (input: I) => Promise<O> | O;
535
+ }
536
+ interface OpenAIToolSpec {
537
+ type: 'function';
538
+ function: {
539
+ name: string;
540
+ description: string;
541
+ parameters: Record<string, unknown>;
542
+ };
543
+ }
544
+ type ToolInvokeResult<O> = {
545
+ status: 'pending';
546
+ approvalId: string;
547
+ expiresAt: string;
548
+ } | {
549
+ status: 'ok';
550
+ result: O;
551
+ };
552
+ interface ApprovalTool<I, O> {
553
+ name: string;
554
+ description: string;
555
+ parameters: Record<string, unknown>;
556
+ openaiSpec(): OpenAIToolSpec;
557
+ invoke(input: I): Promise<Result<ToolInvokeResult<O>>>;
558
+ }
559
+
560
+ interface VerifyOptions {
561
+ /** Override the signing key resolved from `STACKBONE_APPROVAL_SIGNING_KEY` / `approvalSigningKey`. */
562
+ signingKey?: string;
563
+ /** Reject signatures whose timestamp is older than this (seconds). Default 300. */
564
+ toleranceSeconds?: number;
565
+ /** Inject a clock for tests. Defaults to `Date.now`. */
566
+ now?: () => number;
567
+ }
568
+
569
+ type ApprovalTopic = string;
570
+ declare const DECISION_STATUSES: readonly ["approved", "rejected", "timed_out", "cancelled"];
571
+ type DecisionStatus = (typeof DECISION_STATUSES)[number];
572
+ type ApprovalStatus = 'pending' | DecisionStatus;
573
+ type Decision<T = unknown> = {
574
+ status: 'approved' | 'rejected';
575
+ payload: T;
576
+ approver: ApproverInfo;
577
+ decidedAt: string;
578
+ reason?: string;
579
+ } | {
580
+ status: 'timed_out';
581
+ decidedAt: string;
582
+ } | {
583
+ status: 'cancelled';
584
+ decidedAt: string;
585
+ reason?: string;
586
+ };
587
+ interface ApproverInfo {
588
+ id: string;
589
+ email: string;
590
+ name?: string;
591
+ }
592
+ interface ApprovalRequestOptions<T = unknown> {
593
+ topic: ApprovalTopic;
594
+ payload: T;
595
+ title?: string;
596
+ description?: string;
597
+ /** Path on the agent the control plane will POST the decision to (resolved against the agent's public URL server-side). */
598
+ onDecide: string;
599
+ /** ISO 8601 duration (`'24h'`) or milliseconds. Default 24h. */
600
+ timeout?: string | number;
601
+ onTimeout?: 'reject' | 'approve' | 'ignore';
602
+ approver?: string;
603
+ /** Same `(topic, idempotencyKey)` returns the same `approvalId` instead of creating a new one. */
604
+ idempotencyKey?: string;
605
+ metadata?: Record<string, unknown>;
606
+ }
607
+ interface ApprovalRequest {
608
+ approvalId: string;
609
+ status: 'pending';
610
+ callbackUrl: string;
611
+ expiresAt: string;
612
+ }
613
+ interface ApprovalRecord<T = unknown> {
614
+ approvalId: string;
615
+ topic: ApprovalTopic;
616
+ status: ApprovalStatus;
617
+ payload: T;
618
+ decision?: Decision<T>;
619
+ createdAt: string;
620
+ expiresAt: string;
621
+ metadata?: Record<string, unknown>;
622
+ }
623
+ interface ApprovalListOptions {
624
+ status?: ApprovalStatus;
625
+ topic?: ApprovalTopic;
626
+ cursor?: string;
627
+ /** 1..100. Default 50. */
628
+ limit?: number;
629
+ }
630
+ interface ApprovalListResult<T = unknown> {
631
+ items: ApprovalRecord<T>[];
632
+ nextCursor?: string;
633
+ }
634
+ /**
635
+ * `client.approval` — AGENT-LOCAL. The HITL pause is recorded directly in the
636
+ * agent's own `stackbone_platform.approvals` table over the shared
637
+ * `client.database` pool; there is no control-plane POST anymore. The RESUME
638
+ * half stays Studio-driven: a human decides in Studio, the control plane signs
639
+ * an HMAC callback to `callbackUrl`, and the agent reconciles it with
640
+ * `verify()` (local crypto — see `verify.ts`).
641
+ *
642
+ * The write populates only the columns the cloud `create()` used to set
643
+ * (topic/payload/callback_url/idempotency_key/fallback/metadata/timeout_at;
644
+ * `schema` was sourced from a DTO field the SDK options object does not carry,
645
+ * so it stays NULL). `workspace_id`/`agent_id`/`run_id` stay NULL exactly as
646
+ * they were on the control-plane path — the idempotency `ON CONFLICT` relies on
647
+ * `workspace_id` being NULL with `NULLS NOT DISTINCT` (migration 0008).
648
+ */
649
+ declare class ApprovalFacade {
650
+ private readonly _resolved;
651
+ /** Lazy accessor for `client.database` — reaches the shared `Sql` via `.shared().$client`. */
652
+ private readonly _getDatabase;
653
+ constructor(_resolved: ResolvedConfig,
654
+ /** Lazy accessor for `client.database` — reaches the shared `Sql` via `.shared().$client`. */
655
+ _getDatabase: () => DatabaseModule);
656
+ request<T = unknown>(options: ApprovalRequestOptions<T>): Promise<Result<ApprovalRequest>>;
657
+ cancel(approvalId: string, reason?: string): Promise<Result<void>>;
658
+ get<T = unknown>(approvalId: string): Promise<Result<ApprovalRecord<T>>>;
659
+ list<T = unknown>(options?: ApprovalListOptions): Promise<Result<ApprovalListResult<T>>>;
660
+ /**
661
+ * Local crypto verification — does not touch the database, so it has the same
662
+ * shape it had on the control-plane surface. Auditing rule: a method gates
663
+ * iff it issues a datapath call; this one never does.
664
+ */
665
+ verify<T = unknown>(request: Request, options?: VerifyOptions): Promise<Result<Decision<T>>>;
666
+ /**
667
+ * Pure factory — returns an `ApprovalTool` whose `invoke()` ultimately calls
668
+ * back into `ApprovalFacade.request`.
669
+ */
670
+ tool<I, O>(spec: ApprovalToolSpec<I, O>): ApprovalTool<I, O>;
671
+ private sql;
672
+ }
673
+
674
+ declare class ConfigFacade {
675
+ private readonly _resolved;
676
+ /** Lazy accessor for `client.database` — see `SecretsFacade`. */
677
+ private readonly _getDatabase;
678
+ constructor(_resolved: ResolvedConfig,
679
+ /** Lazy accessor for `client.database` — see `SecretsFacade`. */
680
+ _getDatabase: () => DatabaseModule);
681
+ get<T = unknown>(key: string): Promise<Result<T>>;
682
+ /**
683
+ * Keys absent from the agent's config come back as omissions in the
684
+ * response — the returned map only contains entries the agent DB actually
685
+ * has, so callers must access fields with `?.` (the return type is
686
+ * `Partial<T>`).
687
+ */
688
+ getMany<T extends Record<string, unknown> = Record<string, unknown>>(keys: string[]): Promise<Result<Partial<T>>>;
689
+ private loadPayload;
690
+ private sql;
691
+ }
692
+
693
+ /**
694
+ * A prompt's current/active state. Key-based (workspace-unique). `template` is
695
+ * the current version's content; `version` is the live version number.
696
+ */
697
+ interface Prompt {
698
+ key: string;
699
+ name: string;
700
+ description: string | null;
701
+ /** The current version's content. */
702
+ template: string;
703
+ /** The live version number. The first `create` produces version `1`. */
704
+ version: number;
705
+ /** `{{var}}` names referenced by the current version. */
706
+ variables: readonly string[];
707
+ metadata: Record<string, unknown> | null;
708
+ /** ISO 8601 UTC timestamp. */
709
+ createdAt: string;
710
+ updatedAt: string;
711
+ }
712
+ interface GetPromptOptions {
713
+ /** Pin a specific version. Omitted -> current/latest. */
714
+ version?: number;
715
+ }
716
+ interface ListPromptsOptions {
717
+ /** 1..100. Default 50. */
718
+ limit?: number;
719
+ }
720
+ interface ListPromptsResult {
721
+ /** Current version of each (non-deleted) prompt. */
722
+ items: readonly Prompt[];
723
+ }
724
+ interface CreatePromptRequest {
725
+ key: string;
726
+ name: string;
727
+ /** The version-1 content. */
728
+ template: string;
729
+ description?: string;
730
+ metadata?: Record<string, unknown>;
731
+ }
732
+ interface UpdatePromptOptions {
733
+ /** New content. Appends a new immutable version. */
734
+ template?: string;
735
+ name?: string;
736
+ description?: string | null;
737
+ /** Replaces the prompt-head metadata. */
738
+ metadata?: Record<string, unknown>;
739
+ }
740
+ interface DeletePromptOptions {
741
+ /** Reserved for future per-version deletes; ignored today (soft-deletes the key). */
742
+ version?: number;
743
+ }
744
+ interface DeletePromptResult {
745
+ key: string;
746
+ /** Number of prompt heads soft-deleted (0 or 1). */
747
+ deleted: number;
748
+ }
749
+ interface CompilePromptResult {
750
+ /** The rendered string. */
751
+ output: string;
752
+ /** The version the compile ran against. */
753
+ version: number;
754
+ }
755
+ declare class PromptsFacade {
756
+ private readonly _resolved;
757
+ /**
758
+ * Lazy accessor for `client.database`. The facade pulls the shared
759
+ * `postgres-js` `Sql` via `getDatabase().shared().$client` — never
760
+ * reaching into a sibling module's implementation file.
761
+ */
762
+ private readonly _getDatabase;
763
+ constructor(_resolved: ResolvedConfig,
764
+ /**
765
+ * Lazy accessor for `client.database`. The facade pulls the shared
766
+ * `postgres-js` `Sql` via `getDatabase().shared().$client` — never
767
+ * reaching into a sibling module's implementation file.
768
+ */
769
+ _getDatabase: () => DatabaseModule);
770
+ get(key: string, options?: GetPromptOptions): Promise<Result<Prompt>>;
771
+ compile(key: string, variables: Record<string, unknown>, options?: GetPromptOptions): Promise<Result<CompilePromptResult>>;
772
+ list(options?: ListPromptsOptions): Promise<Result<ListPromptsResult>>;
773
+ create(request: CreatePromptRequest): Promise<Result<Prompt>>;
774
+ update(key: string, options: UpdatePromptOptions): Promise<Result<Prompt>>;
775
+ delete(key: string, _options?: DeletePromptOptions): Promise<Result<DeletePromptResult>>;
776
+ private sql;
777
+ private mapReadError;
778
+ private mapWriteError;
779
+ }
780
+
470
781
  /**
471
782
  * Public surface behind `client.ai`. Wraps the official `openai` SDK with a
472
783
  * `baseURL` override pointing to OpenRouter, so 300+ models are reachable
@@ -618,206 +929,47 @@ interface RequestOptions$1 {
618
929
  signal?: AbortSignal;
619
930
  }
620
931
 
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 {
932
+ /**
933
+ * Handle returned by `client.rag.ingestAsync`. The job id is allocated
934
+ * synchronously against `stackbone_rag_jobs` so the caller can track / cancel
935
+ * the work via the `/api/rag/jobs/:jobId/*` surface; `events` is a streaming
936
+ * channel of progress events (ADR §D9 shape); `result` settles when the
937
+ * pipeline finishes (or fails / is cancelled).
938
+ */
939
+ interface IngestAsyncHandle {
672
940
  jobId: string;
673
- error: string;
941
+ events: AsyncIterable<RagIngestProgress>;
942
+ result: Promise<Result<IngestResponse>>;
674
943
  }
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>>;
791
- }
792
- /**
793
- * `client.rag` — `pgvector`-backed retrieval. Two shapes per write/read:
794
- * pass `model` to let the SDK embed for you; pass embeddings precomputed for
795
- * provider/dimension/batching control.
796
- *
797
- * Implementation note: `RagModule` is a thin facade over `RagPipeline`. The
798
- * pipeline owns parse → chunk → embed → persist; the facade resolves the
799
- * shared `client.database` pool, builds an `Embedder` on demand, and maps
800
- * configuration errors. See ADR `2026-05-10-rag-consolidation-on-client-database`.
801
- *
802
- * Connection ownership: this module never opens its own postgres pool. It
803
- * reaches the underlying `postgres-js` `Sql` through the **shared-handles
804
- * pattern** — `client.database.shared().$client` — so an agent that
805
- * touches both surfaces still opens exactly one connection pool against
806
- * `STACKBONE_POSTGRES_URL`. RAG is the canonical first cross-surface
807
- * consumer; memory and queues will adopt the same accessor when they
808
- * land.
809
- *
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
813
- * `SdkError('rag_schema_missing')` with an actionable hint.
814
- */
815
- /**
816
- * Test-only seams accepted by `RagModule`. Production callers leave these
817
- * unset — they are injected by `ingest-async.spec.ts` to substitute the
818
- * shared-pool lookup and the SQL-backed job writer.
819
- */
820
- interface RagModuleTestDeps {
944
+ /**
945
+ * `client.rag` — `pgvector`-backed retrieval. Two shapes per write/read:
946
+ * pass `model` to let the SDK embed for you; pass embeddings precomputed for
947
+ * provider/dimension/batching control.
948
+ *
949
+ * Implementation note: `RagModule` is a thin facade over `RagPipeline`. The
950
+ * pipeline owns parse → chunk → embed → persist; the facade resolves the
951
+ * shared `client.database` pool, builds an `Embedder` on demand, and maps
952
+ * configuration errors. See ADR `2026-05-10-rag-consolidation-on-client-database`.
953
+ *
954
+ * Connection ownership: this module never opens its own postgres pool. It
955
+ * reaches the underlying `postgres-js` `Sql` through the **shared-handles
956
+ * pattern** `client.database.shared().$client` — so an agent that
957
+ * touches both surfaces still opens exactly one connection pool against
958
+ * `STACKBONE_POSTGRES_URL`. RAG is the canonical first cross-surface
959
+ * consumer; memory and queues will adopt the same accessor when they
960
+ * land.
961
+ *
962
+ * Schema readiness: the canonical schema (`stackbone_platform.rag_*`) is
963
+ * provisioned by the platform migrator, present on every install. When an
964
+ * operation still hits `42P01 relation does not exist`, the pipeline returns
965
+ * `SdkError('rag_schema_missing')` with an actionable hint.
966
+ */
967
+ /**
968
+ * Test-only seams accepted by `RagModule`. Production callers leave these
969
+ * unset — they are injected by `ingest-async.spec.ts` to substitute the
970
+ * shared-pool lookup and the SQL-backed job writer.
971
+ */
972
+ interface RagModuleTestDeps {
821
973
  /**
822
974
  * Replaces the `client.database.shared().$client` lookup used by
823
975
  * `ingestAsync`. Tests can plug a fake `Sql` here without standing up
@@ -884,10 +1036,11 @@ declare class RagModule {
884
1036
  deleteWhere(filter: Record<string, unknown>, options?: DeleteOptions): Promise<Result<DeleteResponse>>;
885
1037
  retrieve(request: RetrieveRequest): Promise<Result<RetrieveHit[]>>;
886
1038
  /**
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.
1039
+ * Drops the legacy ad-hoc RAG tables (`rag_chunks` / `_rag_meta`) that
1040
+ * pre-feature-30 agents provisioned on first ingest. Kept for backwards
1041
+ * compatibility so an old agent can clean those up after upgrading. The
1042
+ * canonical RAG schema (`stackbone_platform.rag_*`) is owned by the platform
1043
+ * migrator — this method never touches it.
891
1044
  */
892
1045
  reset(): Promise<Result<void>>;
893
1046
  /**
@@ -917,356 +1070,32 @@ declare class RagModule {
917
1070
  private sql;
918
1071
  private withPipeline;
919
1072
  }
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.
1219
- */
1220
- getMany(names: string[]): Promise<Result<Record<string, string>>>;
1221
- }
1222
-
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 {
1073
+
1074
+ declare class SecretsFacade {
1235
1075
  private readonly _resolved;
1236
- private _processor?;
1237
- private readonly _loggers;
1238
- private _sql;
1239
- private _sqlResolved;
1240
- constructor(_resolved: ResolvedConfig);
1241
1076
  /**
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.
1077
+ * Lazy accessor for `client.database`. The facade pulls the shared
1078
+ * `postgres-js` `Sql` via `getDatabase().shared().$client`never
1079
+ * reaching into a sibling module's implementation file.
1246
1080
  */
1247
- spanProcessor(options?: RunStepsSpanProcessorOptions): RunStepsSpanProcessor;
1248
- flush(): Promise<Result<void>>;
1081
+ private readonly _getDatabase;
1082
+ private _cipher?;
1083
+ constructor(_resolved: ResolvedConfig,
1249
1084
  /**
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.
1085
+ * Lazy accessor for `client.database`. The facade pulls the shared
1086
+ * `postgres-js` `Sql` via `getDatabase().shared().$client` never
1087
+ * reaching into a sibling module's implementation file.
1255
1088
  */
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>;
1089
+ _getDatabase: () => DatabaseModule);
1090
+ get(name: string): Promise<Result<string>>;
1261
1091
  /**
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.
1092
+ * Names absent from the agent come back as omissions in the response the
1093
+ * returned map only contains entries the agent DB actually has. Callers that
1094
+ * need "all-or-nothing" semantics should diff the keys.
1267
1095
  */
1268
- closeRun(runId: string): Promise<Result<AggregateRunCostResult>>;
1269
- private resolveSql;
1096
+ getMany(names: string[]): Promise<Result<Record<string, string>>>;
1097
+ private cipher;
1098
+ private sql;
1270
1099
  }
1271
1100
 
1272
1101
  interface StorageObject {
@@ -1363,28 +1192,190 @@ declare class StorageBucket {
1363
1192
  private resolve;
1364
1193
  }
1365
1194
 
1195
+ /** Subset of `RequestInit['body']` we serialize without modification. */
1196
+ type SerializedBody = NonNullable<RequestInit['body']>;
1197
+ /**
1198
+ * Per-status overrides facades attach when an HTTP status carries a
1199
+ * non-canonical meaning for the endpoint — e.g. a `POST` whose `409` means
1200
+ * "already exists" instead of the default "conflict". Keys are HTTP statuses,
1201
+ * values are domain codes the transport should surface verbatim (no prefix
1202
+ * concatenation — the override is the final code).
1203
+ *
1204
+ * The value is typed against the catalog (`SdkErrorCode`) so an endpoint
1205
+ * cannot smuggle a wire code that isn't in the documented inventory.
1206
+ */
1207
+ type ErrorOverrides = Record<number, SdkErrorCode>;
1208
+ /**
1209
+ * Tells the transport how to remap HTTP status codes onto the surface's
1210
+ * domain code prefix. `404 → '<prefix>_not_found'`, `401 → '<prefix>_unauthorized'`,
1211
+ * etc. Endpoint-specific overrides win over the canonical mapping.
1212
+ *
1213
+ * `prefix` is constrained to `SdkErrorPrefix` (the catalog's prefix tuple) so
1214
+ * an unknown surface name doesn't compile — keeps the wire codes the
1215
+ * transport emits inside the typed inventory.
1216
+ */
1217
+ interface ErrorMapping {
1218
+ /** Prefix concatenated with `_not_found`, `_unauthorized`, …. Required for any domain remap. */
1219
+ prefix: SdkErrorPrefix;
1220
+ /** Per-status final code that overrides the canonical mapping. */
1221
+ overrides?: ErrorOverrides;
1222
+ }
1223
+ interface RequestOptions extends Omit<RequestInit, 'body' | 'signal'> {
1224
+ /** Scalar query params. `undefined` values are dropped, others get coerced via `String()`. */
1225
+ params?: Record<string, string | number | boolean | undefined>;
1226
+ /**
1227
+ * Repeated string-array params validated and serialised by the transport.
1228
+ * Empty arrays / blank elements reject upstream with `<prefix>_invalid_request`
1229
+ * (falls back to `http_invalid_request` when no `errorMapping` is set). Each
1230
+ * array is joined with `,` to match the canonical control-plane shape.
1231
+ */
1232
+ arrayParams?: Record<string, readonly string[]>;
1233
+ body?: SerializedBody | Record<string, unknown> | unknown[] | null;
1234
+ signal?: AbortSignal;
1235
+ /** Allow retrying non-idempotent requests (POST, PATCH). Off by default to prevent duplicate writes. */
1236
+ idempotent?: boolean;
1237
+ }
1238
+ /**
1239
+ * Shape every facade method ideally collapses into: "POST to /path with this
1240
+ * body, validate against this schema, surface errors as `<prefix>_*`". The
1241
+ * transport handles validation, HTTP→domain remapping, querystring assembly
1242
+ * and the `Result` envelope so facades stay tiny orchestrators.
1243
+ *
1244
+ * `arrayParams` keys default to `errorMapping.prefix` for the validation
1245
+ * error code (e.g. an empty `names: []` becomes `secrets_invalid_request`).
1246
+ */
1247
+ interface TransportRequest<T> extends RequestOptions {
1248
+ method: string;
1249
+ path: string;
1250
+ /**
1251
+ * Zod schema (or any object with a `.safeParse()` method) the parsed body
1252
+ * must validate against. On failure the transport surfaces
1253
+ * `<prefix>_invalid_response` (or `http_invalid_response` if no prefix is
1254
+ * configured) with the schema issues attached to `meta.issues`.
1255
+ */
1256
+ responseSchema?: ResponseSchema<T>;
1257
+ /** HTTP→domain remapping. Omit to let `http_*` codes flow through unchanged. */
1258
+ errorMapping?: ErrorMapping;
1259
+ }
1260
+ /**
1261
+ * Minimal contract the transport needs from a schema — `safeParse`. Compatible
1262
+ * with Zod (`z.object(…).safeParse(value)`) and any hand-rolled validator that
1263
+ * follows the same shape. Avoids leaking Zod's full surface into the transport
1264
+ * signature.
1265
+ */
1266
+ interface ResponseSchema<T> {
1267
+ safeParse(input: unknown): {
1268
+ success: true;
1269
+ data: T;
1270
+ } | {
1271
+ success: false;
1272
+ error: {
1273
+ issues: unknown;
1274
+ };
1275
+ };
1276
+ }
1277
+ interface HttpClientOptions {
1278
+ /** Default 30_000. Set to 0 to disable. */
1279
+ timeout?: number;
1280
+ /** Default 3. Set to 0 to disable. */
1281
+ retryCount?: number;
1282
+ /** Initial backoff in ms; doubles each attempt with ±15% jitter. Default 500. */
1283
+ retryDelay?: number;
1284
+ /** Override the global fetch (useful for tests). */
1285
+ fetch?: typeof fetch;
1286
+ }
1366
1287
  /**
1367
- * OAuth connections (Notion, GDrive, Slack…).
1288
+ * Shared HTTP client used by every facade that talks to the Stackbone
1289
+ * control plane. Wraps `fetch` with timeout, exponential backoff with jitter,
1290
+ * idempotent-only retries, response validation (Zod-compatible schemas) and
1291
+ * a uniform `Result` envelope so facades never throw at the SDK boundary.
1368
1292
  *
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.
1293
+ * Each facade tells the transport its error prefix (`'secrets'`, `'config'`,
1294
+ * `'approval'`) so 404/401/403/429/5xx remap to surface-specific codes
1295
+ * `secrets_not_found`, `approval_unauthorized`, etc. Endpoint-specific
1296
+ * remaps (e.g. `409 → 'secrets_already_exists'`) flow through
1297
+ * `errorMapping.overrides`. Transport-level failures (network, timeout,
1298
+ * parse) keep the `http_*` prefix the README contract documents.
1299
+ *
1300
+ * Reads `STACKBONE_API_URL` and `STACKBONE_AGENT_JWT` lazily on every request so
1301
+ * env-var rotation is picked up without restarting the client.
1374
1302
  */
1375
- declare class ConnectionsFacade {
1376
- constructor(_resolved: ResolvedConfig, _http: HttpClient);
1377
- list(): Promise<Result<readonly never[]>>;
1303
+ declare class HttpClient {
1304
+ private readonly resolved;
1305
+ private readonly timeout;
1306
+ private readonly retryCount;
1307
+ private readonly retryDelay;
1308
+ private readonly fetchImpl;
1309
+ constructor(resolved: ResolvedConfig, options?: HttpClientOptions);
1310
+ /**
1311
+ * Convenience for `request({ method: 'GET', path, ... })`. Kept so legacy
1312
+ * callers (tests, pending facades) keep compiling — the deep work happens
1313
+ * in `request()`.
1314
+ */
1315
+ get<T>(path: string, options?: RequestOptions): Promise<Result<T>>;
1316
+ post<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1317
+ put<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1318
+ patch<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1319
+ delete<T>(path: string, options?: RequestOptions): Promise<Result<T>>;
1320
+ /**
1321
+ * The transport entry point every facade should target. Handles base-URL
1322
+ * resolution, header injection, body/querystring serialisation, retry +
1323
+ * timeout, response validation and HTTP→domain error remapping.
1324
+ */
1325
+ request<T>(req: TransportRequest<T>): Promise<Result<T>>;
1326
+ private computeBackoff;
1378
1327
  }
1379
1328
 
1380
- /** Emit events to the organization event bus. */
1381
- declare class EventsFacade {
1329
+ /** Options for `invoke`. */
1330
+ interface InvokeOptions {
1331
+ /**
1332
+ * Selector to disambiguate when the workspace holds several connections of the
1333
+ * same connector. Accepts a connection **id** or a unique **name** (both are
1334
+ * returned by `list()`). Omitted → the proxy resolves the sole connection, or
1335
+ * fails `connections_ambiguous` when there is more than one.
1336
+ */
1337
+ connection?: string;
1338
+ }
1339
+ /**
1340
+ * `client.connections` — the agent's handle on the workspace's connector
1341
+ * connections. The credentials never enter the agent container: each method
1342
+ * makes an authenticated call to the control plane (the connector executor owns
1343
+ * the resolved credential, in a platform process).
1344
+ *
1345
+ * - `list` returns the workspace's connections (connector id, label, auth kind,
1346
+ * health) — never any credential material — so the agent can adapt to what the
1347
+ * customer has connected.
1348
+ * - `invoke(connector, action, args)` runs a connector action imperatively. The
1349
+ * control plane resolves the workspace's connection for `connector`, validates
1350
+ * `args` against the action's input schema, executes it, and returns the
1351
+ * validated output verbatim. `args` is opaque — the SDK forwards it untouched.
1352
+ *
1353
+ * Authentication reuses the existing `STACKBONE_AGENT_JWT` channel injected at
1354
+ * provisioning (the `HttpClient` attaches the bearer + install header), so the
1355
+ * control plane scopes every call to the agent's own install → organization.
1356
+ *
1357
+ * Contract gating: every method awaits the `connections.actions` module gate
1358
+ * before touching the network, so a stale datapath / missing capability surfaces
1359
+ * as a gating error rather than a failed request.
1360
+ */
1361
+ declare class ConnectionsModule {
1382
1362
  private readonly _http;
1383
1363
  private readonly _gate;
1384
1364
  constructor(resolved: ResolvedConfig, _http: HttpClient,
1385
1365
  /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
1386
1366
  gate?: ModuleGate);
1387
- emit(_name: string, _payload: unknown): Promise<Result<void>>;
1367
+ /** List the workspace's connections (no credentials). */
1368
+ list(): Promise<Result<AgentConnectionListResponse>>;
1369
+ /**
1370
+ * Invoke a connector action against the workspace's connection for that
1371
+ * connector. `args` is forwarded opaquely; the control plane validates it
1372
+ * against the action's own input schema and returns the validated output.
1373
+ *
1374
+ * When the workspace holds several connections of the same connector, pass
1375
+ * `{ connection }` (a connection id or unique name) to pick one; omitting it
1376
+ * with more than one connection fails `connections_ambiguous`.
1377
+ */
1378
+ invoke(connector: string, action: string, args: unknown, opts?: InvokeOptions): Promise<Result<InvokeConnectorActionResponse>>;
1388
1379
  }
1389
1380
 
1390
1381
  /**
@@ -1506,95 +1497,66 @@ declare class MemoryModule {
1506
1497
  }
1507
1498
 
1508
1499
  /**
1509
- * A prompt managed by the Stackbone control plane. Templates are plain strings
1510
- * with Mustache-style `{{var}}` placeholdersno conditionals or loops.
1500
+ * Enqueue a one-shot (or deferred) job. The opaque `payload` is whatever the
1501
+ * agent's `receive` handler expects the core never inspects it.
1511
1502
  */
1512
- interface Prompt {
1503
+ interface PublishRequest {
1504
+ /** Opaque job name (e.g. `send-email`). Surfaced in the Studio inspector. */
1513
1505
  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;
1506
+ /** Opaque job payload — JSON-serialisable; the core forwards it untouched. */
1507
+ payload: unknown;
1508
+ /** Per-job override of the platform retry default. Omitted → platform default. */
1509
+ retries?: number;
1510
+ /** Deferred delivery in milliseconds. Omitted / 0 → immediate. */
1511
+ delay?: number;
1512
+ /** Idempotency hint a duplicate publish with the same key collapses to one job. */
1513
+ deduplicationId?: string;
1537
1514
  }
1538
- interface CreatePromptRequest {
1515
+ /** Register / update a dynamic per-installation cron schedule. */
1516
+ interface ScheduleRequest {
1517
+ /** Schedule name, unique per install. Re-scheduling the same name updates it in place. */
1539
1518
  name: string;
1540
- template: string;
1541
- metadata?: Record<string, unknown>;
1542
- }
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>;
1548
- }
1549
- interface DeletePromptOptions {
1550
- /** Delete a specific version. Omitted -> delete every version under `name`. */
1551
- version?: number;
1519
+ /** Opaque payload delivered on every fire. */
1520
+ payload: unknown;
1521
+ /** Cron expression evaluated by the dispatcher. */
1522
+ cron: string;
1523
+ /** IANA timezone the cron pattern runs in. Defaults to UTC. */
1524
+ tz?: string;
1525
+ /** Per-job override of the platform retry default. */
1526
+ retries?: number;
1552
1527
  }
1553
- interface DeletePromptResult {
1528
+ /** Cancel a previously registered schedule by name. */
1529
+ interface UnscheduleRequest {
1554
1530
  name: string;
1555
- /** Number of versions actually removed. */
1556
- deleted: number;
1557
1531
  }
1558
1532
  /**
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.
1533
+ * `client.queues` — the agent's handle on the BullMQ-backed job system. The
1534
+ * agent never touches Redis: each method makes an authenticated call to the
1535
+ * control plane (the dispatcher owns the queue + the clock).
1565
1536
  *
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.
1537
+ * - `publish` enqueues a one-shot job (optionally deferred via `delay`, with a
1538
+ * per-job `retries` override and an optional `deduplicationId`).
1539
+ * - `schedule` / `unschedule` manage dynamic per-user cron schedules.
1540
+ * - `listSchedules` lists the install's active schedules.
1541
+ *
1542
+ * Authentication reuses the existing `STACKBONE_AGENT_JWT` channel injected at
1543
+ * provisioning (the `HttpClient` attaches the bearer + install header), so the
1544
+ * core scopes every enqueue to the agent's own install.
1545
+ *
1546
+ * Contract gating: every method awaits the `queues.jobs` module gate before
1547
+ * touching the network, so a stale datapath / missing capability surfaces as
1548
+ * a gating error rather than a failed POST.
1571
1549
  */
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>>;
1580
- }
1581
-
1582
- interface PublishRequest {
1583
- url: string;
1584
- body: unknown;
1585
- retries?: number;
1586
- delay?: number;
1587
- deduplicationId?: string;
1588
- headers?: Record<string, string>;
1589
- }
1590
1550
  declare class QueuesModule {
1551
+ private readonly _http;
1591
1552
  private readonly _gate;
1592
- constructor(resolved: ResolvedConfig,
1553
+ constructor(resolved: ResolvedConfig, _http: HttpClient,
1593
1554
  /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
1594
1555
  gate?: ModuleGate);
1595
- publish(_request: PublishRequest): Promise<Result<{
1596
- messageId: string;
1597
- }>>;
1556
+ publish(request: PublishRequest): Promise<Result<PublishJobResponse>>;
1557
+ schedule(request: ScheduleRequest): Promise<Result<ScheduleJobResponse>>;
1558
+ unschedule(request: UnscheduleRequest): Promise<Result<UnscheduleJobResponse>>;
1559
+ listSchedules(): Promise<Result<ListSchedulesResponse>>;
1598
1560
  }
1599
1561
 
1600
1562
  /**
@@ -1603,18 +1565,25 @@ declare class QueuesModule {
1603
1565
  * groups each surface by where its state lives:
1604
1566
  *
1605
1567
  * - `surfaces/agent-local/` — Postgres branch owned by the agent
1606
- * (`database`, `rag`).
1568
+ * (`database`, `rag`, `secrets`, `config`, `approval`). `secrets`,
1569
+ * `config` and `approval` moved here from the control plane: the agent now
1570
+ * reads its secrets/config and records HITL pauses directly against its own
1571
+ * `stackbone_platform.*` tables (decrypting secrets with the per-agent key),
1572
+ * with no control-plane round-trip. The approval RESUME callback stays
1573
+ * Studio-signed and is reconciled locally by `approval.verify()`.
1607
1574
  * - `surfaces/external/` — managed partner SDKs the SDK wraps directly
1608
- * (`ai` / OpenRouter, `storage` / S3 + R2 / MinIO, `observability` /
1609
- * OpenTelemetry).
1575
+ * (`ai` / OpenRouter, `storage` / S3 + R2 / MinIO). Observability is not a
1576
+ * creator surface: agent code observes runs through the handler-scoped
1577
+ * `ctx.logger` (and plain `console.*`, which the wrapper captures and
1578
+ * correlates), while the platform primitives live in the
1579
+ * `@stackbone/sdk/observability` subpath the runtime wires directly.
1610
1580
  * - `surfaces/control-plane/` — thin HTTP calls to Stackbone's control
1611
- * plane (`approval`, `secrets`, `config`).
1581
+ * plane (none today after secrets/config/approval moved agent-local).
1612
1582
  * - `surfaces/pending/` — public surface is stable but the runtime is
1613
1583
  * not built yet. They are wired as live `client.X` accessors so MVP
1614
1584
  * agent code can be authored against the eventual contract today; each
1615
1585
  * method returns `Result<{ error: { code: "not_implemented" } }>` until
1616
- * the backing runtime ships. Today: queues, memory, prompts,
1617
- * connections, events.
1586
+ * the backing runtime ships. Today: queues, memory.
1618
1587
  *
1619
1588
  * Each accessor builds its surface on first access and caches it — env
1620
1589
  * vars and partner SDK init costs are paid only when the agent actually
@@ -1632,15 +1601,13 @@ declare class StackboneClient {
1632
1601
  private _storage?;
1633
1602
  private _ai?;
1634
1603
  private _rag?;
1635
- private _observability?;
1636
1604
  private _approval?;
1637
1605
  private _secrets?;
1638
1606
  private _config?;
1639
1607
  private _queues?;
1608
+ private _connections?;
1640
1609
  private _memory?;
1641
- private _events?;
1642
1610
  private _prompts?;
1643
- private _connections?;
1644
1611
  private _httpClient?;
1645
1612
  private readonly _contractStore;
1646
1613
  constructor(resolved: ResolvedConfig);
@@ -1649,18 +1616,27 @@ declare class StackboneClient {
1649
1616
  get storage(): StorageModule;
1650
1617
  get ai(): AiModule;
1651
1618
  get rag(): RagModule;
1652
- get observability(): ObservabilityModule;
1653
1619
  get approval(): ApprovalFacade;
1654
1620
  get secrets(): SecretsFacade;
1655
1621
  get config(): ConfigFacade;
1656
1622
  /**
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.
1623
+ * Live surface — the agent's handle on the BullMQ-backed job system.
1624
+ * `publish` / `schedule` / `unschedule` / `listSchedules` make authenticated
1625
+ * calls to the control-plane dispatcher (`/api/v1/agent/queues/*`); the
1626
+ * agent never touches Redis. Gated against `queues.jobs` so the
1627
+ * handshake-blocked / capability-missing paths are exercised the same way
1628
+ * as the other gated surfaces.
1662
1629
  */
1663
1630
  get queues(): QueuesModule;
1631
+ /**
1632
+ * Live surface — the agent's handle on the workspace's connector connections.
1633
+ * `list` / `invoke` make authenticated calls to the control-plane connectors
1634
+ * proxy (`/api/v1/agent/connections/*`); the credentials never enter the agent
1635
+ * container. Gated against `connections.actions` so the handshake-blocked /
1636
+ * capability-missing paths are exercised the same way as the other gated
1637
+ * surfaces.
1638
+ */
1639
+ get connections(): ConnectionsModule;
1664
1640
  /**
1665
1641
  * Pending surface — runtime not built. Every method returns
1666
1642
  * `not_implemented`. Mem0 is a third-party integration, not a Stackbone
@@ -1668,23 +1644,14 @@ declare class StackboneClient {
1668
1644
  */
1669
1645
  get memory(): MemoryModule;
1670
1646
  /**
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.
1647
+ * Live agent-local surface — reads prompts from `stackbone_platform.prompts`
1648
+ * in the agent DB over the shared `client.database` pool and compiles
1649
+ * templates with the local `@stackbone/prompt-compiler`. Constructed with
1650
+ * the same `(resolved, () => this.database)` shape as `secrets`/`config`;
1651
+ * like them it is NOT wired to the control-plane handshake gate (it talks to
1652
+ * the agent's own tables, not a control-plane datapath).
1680
1653
  */
1681
1654
  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
1655
  /**
1689
1656
  * Read-only view of the most recently resolved Stackbone Agent Protocol
1690
1657
  * contract for this client's `stackboneApiUrl`. Returns `null` until a
@@ -1781,13 +1748,13 @@ interface InvokeContext<I extends z.ZodType> {
1781
1748
  logger: Logger;
1782
1749
  }
1783
1750
  /**
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.
1751
+ * The synchronous `invoke` capability. Mirrors the wrapper's three HTTP verbs
1752
+ * (`/invoke`, `/health`, `/schema`): `invoke` is the only request path, its
1753
+ * `output` is the `/invoke` response body, and its schemas drive `/schema`.
1787
1754
  *
1788
1755
  * 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.
1756
+ * hatch) so the precise type the creator gets from `defineAgent` rejects
1757
+ * unknown keys on the block at compile time.
1791
1758
  */
1792
1759
  interface InvokeCapability<I extends z.ZodType, O extends z.ZodType> {
1793
1760
  /** Zod schema for the parsed `/invoke` request body. */
@@ -1796,47 +1763,232 @@ interface InvokeCapability<I extends z.ZodType, O extends z.ZodType> {
1796
1763
  output: O;
1797
1764
  /** Creator-supplied handler. Returns either the parsed output or a promise of it. */
1798
1765
  run: (ctx: InvokeContext<I>) => z.infer<O> | Promise<z.infer<O>>;
1799
- /** Optional per-invocation timeout in milliseconds. Wired by a later slice. */
1766
+ /** Optional per-invocation timeout in milliseconds. */
1767
+ timeoutMs?: number;
1768
+ }
1769
+ /**
1770
+ * A named **job handler** — a sibling of `invoke` at the top level of the spec,
1771
+ * reached asynchronously via `client.queues.publish({ name })` (the BullMQ
1772
+ * dispatcher pushes the job back to `/invoke` with that name; the wrapper routes
1773
+ * it here instead of to `invoke.run`).
1774
+ *
1775
+ * Same shape as `invoke` with one difference: `output` is **optional**. A job is
1776
+ * fire-and-forget — the dispatcher only checks the 2xx, the return value is
1777
+ * journalled, not delivered to a caller. Declaring `output` self-validates the
1778
+ * job's result; omitting it lets `run` return anything (or nothing).
1779
+ */
1780
+ interface JobCapability<I extends z.ZodType, O extends z.ZodType = z.ZodType> {
1781
+ /** Zod schema for the job payload (`publish`'s `payload`, revalidated here). */
1782
+ input: I;
1783
+ /** Optional Zod schema for the job's return value. Validated + journalled, never returned. */
1784
+ output?: O;
1785
+ /** Creator-supplied job handler. May return nothing — the result is not delivered. */
1786
+ run: (ctx: InvokeContext<I>) => z.infer<O> | void | Promise<z.infer<O> | void>;
1787
+ /** Optional per-job timeout in milliseconds. */
1800
1788
  timeoutMs?: number;
1801
1789
  }
1802
1790
  /**
1803
- * The declarative shape every Stackbone agent returns from `src/index.ts`.
1791
+ * Loose, non-generic view of a single capability (invoke or a job handler).
1792
+ * The wrapper / `loadSpec` read the spec through this — the precise per-handler
1793
+ * typing lives on the value `defineAgent` returns to the creator, not here.
1804
1794
  *
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`.
1795
+ * `run` is a method signature (not an arrow property) on purpose: it makes the
1796
+ * parameter bivariant so every concrete `InvokeCapability` / `JobCapability`
1797
+ * (whose `run` is typed against its own narrow input) is assignable to it, which
1798
+ * is what lets `AgentSpec` carry a string index signature of capabilities.
1799
+ */
1800
+ interface AnyCapability {
1801
+ input: z.ZodType;
1802
+ output?: z.ZodType | undefined;
1803
+ run(ctx: InvokeContext<z.ZodType>): unknown;
1804
+ timeoutMs?: number | undefined;
1805
+ }
1806
+ /**
1807
+ * The declarative shape every Stackbone agent returns from `src/index.ts`, as
1808
+ * the wrapper / `loadSpec` consume it: the **loose runtime view**. Every
1809
+ * capability — `invoke` and each named job handler — is an `AnyCapability`
1810
+ * (`output` optional, `run` loose). `invoke` is required; the string index
1811
+ * carries the job handlers, reached by name through the BullMQ dispatcher.
1809
1812
  *
1810
- * `health` and other capabilities will join `invoke` as they land — every
1811
- * addition is an explicit, reviewable edit to this type.
1813
+ * The precise per-handler typing (each `ctx.input` inferred, `invoke.output`
1814
+ * required) lives on the value `defineAgent` returns, which is assignable to
1815
+ * this. At runtime `invoke.output` is always present — the spec seed forces it —
1816
+ * so the wrapper reads it behind a small guard.
1812
1817
  */
1813
- interface AgentSpec<I extends z.ZodType, O extends z.ZodType> {
1814
- invoke: InvokeCapability<I, O>;
1818
+ interface AgentSpec {
1819
+ /** The synchronous entry: the `/invoke` response + the `GET /schema` source. */
1820
+ invoke: AnyCapability;
1821
+ /** Named job handlers, reached via `client.queues.publish({ name })`. */
1822
+ [name: string]: AnyCapability;
1815
1823
  }
1816
1824
  /**
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
1825
+ * Top-level keys the platform owns. A job handler may not use these names.
1826
+ * Only `invoke` for now; the contract adds `health` / `schema` / `events` /
1827
+ * `schedules` here if/when they become declarable capabilities (see the named
1828
+ * job handlers ADR).
1829
+ */
1830
+ declare const RESERVED_HANDLER_NAMES: readonly ["invoke"];
1831
+ /**
1832
+ * Hard ceiling on any capability's `timeoutMs`. The platform saga kills the
1833
+ * Machine before the wrapper would time out at higher values, so a creator
1834
+ * declaring a 10-minute timeout would publish a contract the runtime cannot
1835
+ * keep. Surface that conflict at boot instead of as a mysterious mid-invocation
1821
1836
  * SIGKILL in production.
1822
1837
  */
1823
1838
  declare const INVOKE_TIMEOUT_HARD_CAP_MS = 300000;
1824
1839
  /**
1825
- * Identity function with two jobs:
1840
+ * One handler block, generic **directly** on its input Zod schema `I`. `input: I`
1841
+ * is a single-level type parameter (not a nested indexed access like
1842
+ * `S['input']`), which is the shape TypeScript can invert when reading the spec
1843
+ * literal back — so every handler's `ctx.input` is inferred from its own schema.
1844
+ * `output` stays loose (`z.ZodType`) and `run`'s return is `unknown`: a handler's
1845
+ * result is validated against `output` at runtime (the wrapper), not at the type
1846
+ * layer, so output inference is not needed for the contract to hold.
1847
+ */
1848
+ type HandlerBlock<I extends z.ZodType> = {
1849
+ input: I;
1850
+ output?: z.ZodType;
1851
+ run: (ctx: InvokeContext<I>) => unknown | Promise<unknown>;
1852
+ timeoutMs?: number;
1853
+ };
1854
+ /**
1855
+ * Per-key precise shape of the spec the creator writes. `T` maps each handler
1856
+ * name to its **input** schema; `[K in keyof T]: HandlerBlock<T[K]>` is a
1857
+ * homomorphic mapped type whose value (`input: T[K]`) is a single-level access,
1858
+ * so TypeScript infers `T` back from the literal and keeps the concrete key
1859
+ * names. The `& { invoke: ... }` arm makes `invoke` required and forces its
1860
+ * `output` to be present (a job's `output` is optional).
1861
+ */
1862
+ type AgentSpecParam<T extends Record<string, z.ZodType>> = {
1863
+ [K in keyof T]: HandlerBlock<T[K]>;
1864
+ } & {
1865
+ invoke: {
1866
+ input: z.ZodType;
1867
+ output: z.ZodType;
1868
+ };
1869
+ };
1870
+ /**
1871
+ * Anchor + guard for `defineAgent`:
1826
1872
  *
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.
1873
+ * 1. **Inference anchor** — each handler (`invoke` and every named job) infers
1874
+ * its own `ctx.input` from its `input` schema. The creator never repeats a
1875
+ * type.
1876
+ * 2. **Boot-time invariant** — `invoke` is required; every other top-level key
1877
+ * must be a valid handler object reached via `client.queues.publish({ name })`.
1878
+ * The TypeScript signature already shapes this; the runtime guard is the
1879
+ * safety net for creators forcing a shape through `as any`.
1835
1880
  *
1836
1881
  * Returns the spec verbatim so callers can `export default defineAgent({...})`
1837
1882
  * and the wrapper can read the same object back through dynamic import.
1838
1883
  */
1839
- declare const defineAgent: <I extends z.ZodType, O extends z.ZodType>(spec: AgentSpec<I, O>) => AgentSpec<I, O>;
1884
+ declare function defineAgent<T extends Record<string, z.ZodType>>(spec: AgentSpecParam<T>): {
1885
+ [K in keyof T]: HandlerBlock<T[K]>;
1886
+ };
1887
+
1888
+ interface InvocationContext {
1889
+ /** The envelope's `invocationId` — surfaced on log lines as `trace_id`. */
1890
+ readonly invocationId: string;
1891
+ /** The run this invoke belongs to — surfaced on log lines as `run_id`. */
1892
+ readonly runId: string;
1893
+ /**
1894
+ * The handler this invoke routed to — `invoke` for a direct entry or the
1895
+ * named job handler (e.g. `demo-log-job`) the dispatcher delivered to.
1896
+ * Surfaced on log lines as `handler` so the Studio logs trail can tell which
1897
+ * entrypoint emitted each line. Optional so a caller that binds a context
1898
+ * without routing info (older tests, lifecycle hooks) still type-checks.
1899
+ */
1900
+ readonly handler?: string;
1901
+ }
1902
+ /**
1903
+ * Runs `fn` with `context` bound as the active invocation. The context stays
1904
+ * available to every synchronous and asynchronous continuation rooted in this
1905
+ * call (i.e. across `await`s inside `fn`). Returns whatever `fn` returns.
1906
+ */
1907
+ declare function runWithInvocationContext<T>(context: InvocationContext, fn: () => T): T;
1908
+ /**
1909
+ * Returns the active invocation context, or `undefined` when called outside any
1910
+ * invoke (process boot, lifecycle events, tests that don't set one up).
1911
+ */
1912
+ declare function getInvocationContext(): InvocationContext | undefined;
1913
+
1914
+ /** The subset of the global `console` this bridge replaces. */
1915
+ interface ConsoleLike {
1916
+ log: (...args: unknown[]) => void;
1917
+ info: (...args: unknown[]) => void;
1918
+ warn: (...args: unknown[]) => void;
1919
+ error: (...args: unknown[]) => void;
1920
+ debug: (...args: unknown[]) => void;
1921
+ }
1922
+ interface InstallConsoleCaptureOptions {
1923
+ /** Console object to patch. Defaults to the global `console`. Test seam. */
1924
+ console?: ConsoleLike;
1925
+ /** Sink for `log` / `info` / `debug`. Defaults to `process.stdout`. Test seam. */
1926
+ stdout?: LogSink;
1927
+ /** Sink for `warn` / `error`. Defaults to `process.stderr`. Test seam. */
1928
+ stderr?: LogSink;
1929
+ /** Clock for the `ts` field. Defaults to `() => new Date()`. Test seam. */
1930
+ now?: () => Date;
1931
+ }
1932
+ /**
1933
+ * Replaces `console.log/info/debug/warn/error` so that, while an invocation
1934
+ * context is active, each call is re-emitted as a structured log line carrying
1935
+ * `run_id` / `trace_id`. Returns a function that restores the originals.
1936
+ * Calling it twice on the same console is a no-op (returns a no-op restore).
1937
+ */
1938
+ declare function installInvocationConsoleCapture(options?: InstallConsoleCaptureOptions): () => void;
1939
+
1940
+ declare const STORED_TO_SDK_ENV: Readonly<Record<string, string>>;
1941
+ /** Encrypted envelope row as stored in `stackbone_platform.secrets`. */
1942
+ interface SystemSecretRow {
1943
+ name: string;
1944
+ version: string;
1945
+ nonce: Buffer | Uint8Array;
1946
+ ciphertext: Buffer | Uint8Array;
1947
+ }
1948
+ /**
1949
+ * Maps the `is_system` rows to the SDK env names, decrypting ONLY the rows that
1950
+ * have a known mapping, then derives two env-only channels. Existing values are
1951
+ * never overwritten (an explicit container/Machine override always wins). The
1952
+ * SDK env names this call populated are returned (sorted) so the caller can log
1953
+ * an observable, value-free "what got rehydrated" line — values are never
1954
+ * logged. Shared by both the cloud neon reader and the postgres-js reader so
1955
+ * they cannot drift on the mapping.
1956
+ *
1957
+ * Decryption is lazy on purpose: an unmapped stored row is skipped WITHOUT
1958
+ * decrypting it, so a non-envelope row (e.g. a probe row a stubbed query
1959
+ * returns) never throws. The two derived channels:
1960
+ * - `STACKBONE_POSTGRES_URL` from `databaseUrl` (the SDK's `client.database`
1961
+ * reads this name; the cloud harness's own Neon driver keeps `DATABASE_URL`).
1962
+ * - `STACKBONE_APPROVAL_SIGNING_KEY` from `HMAC_SECRET` so the SDK's approval
1963
+ * callback verifier reconciles with the signer (which signs with the same
1964
+ * boot-bundle HMAC).
1965
+ */
1966
+ declare function rehydrateSystemSecretsRows(env: NodeJS.ProcessEnv, rows: readonly SystemSecretRow[], cipher: SecretCipher, databaseUrl: string): string[];
1967
+ /**
1968
+ * Reads the `is_system` rows from `stackbone_platform.secrets`. Injectable so
1969
+ * tests run against a stub without a real Postgres; production opens a
1970
+ * short-lived `postgres-js` connection (see `loadSystemSecretsIntoEnv`).
1971
+ */
1972
+ type SystemSecretsReader = () => Promise<SystemSecretRow[]>;
1973
+ interface LoadSystemSecretsArgs {
1974
+ /** Connection string for the agent's Postgres (`STACKBONE_POSTGRES_URL`). */
1975
+ readonly databaseUrl: string;
1976
+ /** Per-agent base64 key the rows are decrypted with (`STACKBONE_SECRET_KEY`). */
1977
+ readonly secretKey: string;
1978
+ /** Target env to populate. Defaults to `process.env`. */
1979
+ readonly env?: NodeJS.ProcessEnv;
1980
+ /** Override the row reader (tests inject a stub). */
1981
+ readonly reader?: SystemSecretsReader;
1982
+ }
1983
+ /**
1984
+ * Reads every `is_system` row from the agent's Postgres via `postgres-js`,
1985
+ * decrypts each with the per-agent key, and rehydrates them into `env` under
1986
+ * the SDK's expected names. Used by the `stackbone dev` emulator and the
1987
+ * self-host runtime entry to give the agent the same credentials the cloud
1988
+ * harness rehydrates — without depending on a Neon HTTP endpoint, which local
1989
+ * dev does not have.
1990
+ */
1991
+ declare function loadSystemSecretsIntoEnv(args: LoadSystemSecretsArgs): Promise<string[]>;
1840
1992
 
1841
1993
  /**
1842
1994
  * Reserved error codes. Order is intentional: keep them flat (string) so
@@ -1968,4 +2120,4 @@ interface JsonSchemaDocument {
1968
2120
  */
1969
2121
  declare const analyzeAgentSchemas: (pair: AgentSchemaPair) => SchemaIntrospectionResult;
1970
2122
 
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 };
2123
+ 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 InvokeOptions, 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 };