Plugin SDK Core V1 and Secrets Local Provider Extraction
Problem
The broader Plugin SDK planning epic defines the destination: Signet plugins as cross-surface capability modules that can extend daemon, CLI, MCP, dashboard, SDK, connectors, and prompt lifecycle surfaces.
That full destination is intentionally larger than a first implementation. If we try to implement TypeScript plugins, Rust sidecars, marketplace install, dynamic UI mounting, prompt composition, and every secret provider at once, the PR becomes an everything-bagel and the trust boundary gets blurry.
V1 should prove the architecture with the smallest useful slice:
- a daemon-owned plugin host for bundled TypeScript core plugins,
- a manifest and registry model that will survive marketplace support later,
- prompt contribution plumbing with visibility and disable behavior,
- surface metadata plumbing for CLI/MCP/dashboard/connectors without requiring dynamic loading everywhere yet,
signet.secretsrepresented as a privileged core plugin,- the current local encrypted secrets implementation extracted behind a local provider interface without changing existing user data.
Goals
- Add a plugin host skeleton owned by the daemon.
- Support bundled TypeScript core plugin manifests.
- Persist plugin registry and lifecycle state.
- Expose plugin status and diagnostics through daemon API.
- Support append/context prompt contributions with provenance, token budgets, ordering, and disable behavior.
- Add a surface metadata registry for daemon, CLI, MCP, dashboard, SDK, and connector contributions.
- Represent Signet Secrets as the first privileged bundled core plugin.
- Extract local secret storage behind a provider interface while preserving
secrets.encbyte-for-byte unless a user writes a new/updated secret. - Keep existing
/api/secrets/*, CLI, MCP, dashboard, and SDK behavior working. - Store marketplace-ready manifest metadata without implementing marketplace install.
Non-Goals
- No marketplace install, review, ranking, payments, or public discovery.
- No third-party plugin execution.
- No Rust sidecar execution in V1.
- No WASI runtime.
- No native dynamic-library plugin loading.
- No dynamic dashboard panel rendering from arbitrary plugin code.
- No dynamic CLI command loading from arbitrary plugin code.
- No Bitwarden, Vault, AWS, GCP, Azure, pass/gopass, or env provider implementation.
- No secret store format migration.
- No raw secret read endpoint.
- No plugin-authored mutation of user prompts beyond append/context contributions.
- No removal of legacy secrets compatibility routes.
Architecture
Daemon
+-- plugin host
| +-- manifest validator
| +-- registry store
| +-- lifecycle state
| +-- capability grants
| +-- surface metadata registry
| +-- prompt contribution registry
| +-- health/status diagnostics
|
+-- bundled core plugins
| +-- signet.secrets
|
+-- existing API/CLI/MCP/dashboard/connectors
+-- continue calling existing compatibility surfaces
+-- can read plugin status/metadata where useful
V1 does not make every Signet surface dynamically plugin-rendered. Instead, it creates the host and metadata contract those surfaces will later consume. Existing first-party surfaces remain hand-wired where necessary, but they are associated with the plugin that owns them.
Plugin Manifest Contract
V1 manifests are data contracts, not arbitrary execution permissions.
Required fields:
interface PluginManifestV1 {
readonly id: string;
readonly name: string;
readonly version: string;
readonly publisher: string;
readonly description: string;
readonly runtime: PluginRuntimeV1;
readonly compatibility: PluginCompatibilityV1;
readonly trustTier: PluginTrustTier;
readonly capabilities: readonly string[];
readonly surfaces: PluginSurfaceDeclarationsV1;
readonly marketplace?: PluginMarketplaceMetadataV1;
readonly docs: PluginDocsMetadataV1;
}
Runtime in V1:
interface PluginRuntimeV1 {
readonly language: "typescript" | "rust";
readonly kind: "bundled-module" | "sidecar" | "wasi" | "host-managed";
readonly entry?: string;
readonly protocol?: string;
}
V1 only executes:
language=typescript
kind=bundled-module
trustTier=core
V1 can also activate host-managed verified/core plugin metadata when the
implementation is native Signet code and no external plugin runtime is executed.
Rust, sidecar, and WASI manifest fields are accepted for forward-compatible
metadata and status reporting, but those plugins enter blocked with an
unsupported-runtime reason until later specs implement execution.
Validation rules:
idis stable and globally unique.versionis SemVer.publisheris required.compatibility.signetandcompatibility.pluginApiare required.- Every declared surface must map to at least one declared capability.
- Every declared capability must have docs metadata.
- Only Signet-owned bundled metadata can mark a plugin as
trustTier=core. - Unsupported runtimes are recorded but not started.
Registry and Persistence Contract
The daemon persists plugin state. The implementation may use SQLite or a JSON file in V1, but it must expose the same logical fields.
Logical record:
interface PluginRegistryRecordV1 {
readonly id: string;
readonly name: string;
readonly version: string;
readonly publisher: string;
readonly source: "bundled" | "local" | "marketplace";
readonly trustTier: "core" | "verified" | "community" | "local-dev";
readonly enabled: boolean;
readonly state: "installed" | "blocked" | "active" | "degraded" | "disabled";
readonly stateReason?: string;
readonly grantedCapabilities: readonly string[];
readonly pendingCapabilities: readonly string[];
readonly surfaces: PluginSurfaceSummaryV1;
readonly health?: PluginHealthV1;
readonly installedAt: string;
readonly updatedAt: string;
}
Persistence rules:
- Bundled core plugins are discovered on daemon startup.
- Discovery is idempotent.
- Removing a bundled plugin from the binary marks it unavailable; it does not delete plugin-owned user data.
- Disabled plugins do not contribute prompts or active surface metadata.
- Blocked plugins expose a clear
stateReason. - Degraded plugins remain registered and visible in diagnostics.
Lifecycle Contract
V1 states:
installed -> blocked | disabled | active -> degraded
Rules:
- Unsupported runtime means
blocked. - Missing dependency means
blocked. - Health failure means
degraded. - User/admin disable means
disabled. disabledremoves prompt contributions and active surface metadata.degradeddoes not crash the daemon.- Core plugins may be non-removable but can still report degraded/disabled where safe.
Capability and Grant Contract
Capabilities are declared by a manifest and granted by host policy.
For V1:
- bundled core plugins may receive bundled grants,
- unsupported plugins receive no grants,
- marketplace/local installs are metadata-only and cannot execute,
- capability checks are enforced for plugin-owned daemon routes where the host mounts them,
- compatibility routes may continue using existing auth while recording their owning plugin in diagnostics.
Required signet.secrets capabilities:
secrets:list
secrets:write
secrets:delete
secrets:exec
secrets:providers:list
secrets:providers:configure
prompt:contribute:user-prompt-submit
mcp:tool
cli:command
dashboard:panel
sdk:client
connector:capability
The grant model must distinguish:
declaredCapabilities != grantedCapabilities
Even for bundled plugins, diagnostics should show both.
Surface Metadata Registry
V1 stores and exposes surface metadata. It does not require every consumer to be fully dynamic yet.
Surface metadata includes:
interface PluginSurfaceSummaryV1 {
readonly daemonRoutes: readonly PluginRouteSummaryV1[];
readonly cliCommands: readonly PluginCommandSummaryV1[];
readonly mcpTools: readonly PluginToolSummaryV1[];
readonly dashboardPanels: readonly PluginDashboardSummaryV1[];
readonly sdkClients: readonly PluginSdkSummaryV1[];
readonly connectorCapabilities: readonly PluginConnectorSummaryV1[];
readonly promptContributions: readonly PluginPromptSummaryV1[];
}
Rules:
- Disabled plugins have no active surface metadata.
- Blocked plugins can show planned surfaces but not active surfaces.
- Existing first-party CLI/MCP/dashboard surfaces may remain hand-wired but
should be represented in metadata under
signet.secrets. - Surface metadata includes docs/help text.
- Surface metadata never includes secret values or provider tokens.
Prompt Contribution Contract
V1 supports static prompt contributions from bundled core plugins.
Contribution shape:
interface PromptContributionV1 {
readonly id: string;
readonly pluginId: string;
readonly target: "system" | "session-start" | "user-prompt-submit";
readonly mode: "append" | "context";
readonly priority: number;
readonly maxTokens: number;
readonly content: string;
}
Ordering bands:
| Priority band | Owner |
|---|---|
| 0-99 | Signet core invariants |
| 100-199 | user identity |
| 200-299 | runtime/connectors |
| 300-399 | memory |
| 400-499 | plugin advisory context |
Rules:
- V1 plugin contributions default to
400-499. - Contributions are append/context only.
- Contributions cannot suppress or replace user identity files.
- Contributions are clipped to
maxTokensbefore global prompt clipping. - Prompt diagnostics list included and excluded contributions.
- Disabling the owning plugin removes the contribution without daemon restart if the prompt registry is re-read at request time, or after daemon restart if V1 implementation chooses startup-only registry loading. The chosen behavior must be documented.
Required Secrets contribution:
When the user provides credentials or a task requires reusable credentials,
prefer storing them in Signet Secrets rather than chat, memory, logs, or source
files. Use secret_exec or provider-backed secret references when commands need
credentials.
Plugin Diagnostics API
V1 adds daemon diagnostics endpoints. Exact paths may be adjusted to match route organization, but the response contracts must be stable.
Required endpoints:
GET /api/plugins
GET /api/plugins/:id
GET /api/plugins/:id/diagnostics
GET /api/plugins/prompt-contributions
GET /api/plugins response:
interface PluginListResponseV1 {
readonly plugins: readonly PluginRegistryRecordV1[];
}
GET /api/plugins/prompt-contributions response:
interface PromptContributionListResponseV1 {
readonly contributions: readonly PromptContributionV1[];
readonly activeCount: number;
}
Rules:
- Diagnostics never include raw secret values.
- Diagnostics identify disabled/blocked/degraded reasons.
- Diagnostics identify active prompt contributors by plugin ID.
- Diagnostics identify compatibility routes owned by plugins.
Secrets Plugin V1
signet.secrets is a bundled privileged core plugin.
It owns metadata for:
/api/secrets/*routes,signet secretCLI commands,- Signet MCP secret tools,
- dashboard Secrets settings panel,
- SDK secret helpers,
- connector-visible secret capabilities,
- Secrets prompt contribution.
V1 implementation may keep route/controller code in its current package layout
if the plugin host records signet.secrets as the owner. The important V1
change is the capability boundary and local provider extraction, not a cosmetic
file move.
Local Secrets Provider Extraction
The current local encrypted store becomes a provider implementation under
signet.secrets.
Provider interface:
interface LocalSecretProviderV1 {
readonly id: "local";
list(ctx: SecretContextV1): Promise<readonly SecretDescriptorV1[]>;
put(name: string, value: string, ctx: SecretContextV1): Promise<void>;
delete(name: string, ctx: SecretContextV1): Promise<boolean>;
resolve(ref: SecretRefV1, ctx: SecretContextV1): Promise<ResolvedSecretV1>;
health(ctx: SecretContextV1): Promise<SecretProviderHealthV1>;
}
Compatibility invariant:
Existing $SIGNET_WORKSPACE/.secrets/secrets.enc files remain valid without
migration, re-encryption, relocation, or user action.
V1 must preserve:
file: $SIGNET_WORKSPACE/.secrets/secrets.enc
format: version 1 JSON wrapper with per-secret ciphertext
crypto: libsodium secretbox
key: BLAKE2b-256 of signet:secrets:{machine-id}
Rules:
- Startup must not rewrite
secrets.enc. - Listing secrets must not decrypt every value unless necessary.
- Resolve happens only inside the daemon/plugin/provider boundary.
- Command execution redacts resolved values from stdout/stderr.
- Corrupt or machine-mismatched stores fail clearly and are never overwritten automatically.
- Writes may update
secrets.encusing the existing format. - Existing bare names keep working as local references.
Secrets Compatibility Routes
Existing routes remain available:
GET /api/secrets
POST /api/secrets/:name
DELETE /api/secrets/:name
POST /api/secrets/exec
GET /api/secrets/exec/:jobId
POST /api/secrets/:name/exec
GET /api/secrets/1password/status
POST /api/secrets/1password/connect
DELETE /api/secrets/1password/connect
GET /api/secrets/1password/vaults
POST /api/secrets/1password/import
V1 does not need to convert 1Password into a provider, but it must not regress
1Password behavior. If 1Password remains on the current implementation path, the
plugin diagnostics should mark it as compatibility-owned by signet.secrets and
future-provider pending.
Secret Reference and Alias V1
V1 must support:
OPENAI_API_KEY == local://OPENAI_API_KEY
Provider-qualified syntax for future providers may be accepted in parsers, but
only local:// is required to resolve in V1.
Resolution order in V1:
local://NAME- bare
NAMEas local compatibility lookup
User-defined aliases may be deferred. If implemented in V1, they must follow the broader planning spec rules: provider-qualified target, audit event, and loop rejection.
Audit Events V1
V1 must emit audit or structured diagnostic events for:
plugin.discovered
plugin.enabled
plugin.disabled
plugin.blocked
plugin.degraded
plugin.health_failed
prompt.contribution_added
prompt.contribution_removed
secret.listed
secret.stored
secret.deleted
secret.resolved_for_exec
secret.exec_started
secret.exec_completed
Rules:
- Secret values are never logged.
- Command stdout/stderr are not audit payloads.
- Event payloads include plugin ID, timestamp, result, and agent scope where available.
- Secret names may be included only where current API behavior already exposes them or policy allows them.
Rollback and Degraded Mode
V1 rollback depends on not rewriting user data.
Rules:
- The plugin host migration does not rewrite
secrets.enc. - If plugin registry loading fails, the daemon should still be able to mount existing secrets routes through the local provider compatibility path.
- If
signet.secretsis degraded, diagnostics must say whether local secrets are available, unavailable, or blocked by key mismatch/corruption. - If prompt contribution loading fails, prompt-submit continues without plugin contributions and records degraded diagnostics.
- Disabling
signet.secretsremoves prompt guidance and connector/MCP advertising, but must not delete stored secrets.
Implementation Phases
Phase 1: Host and Registry
- Add manifest types and validation.
- Add plugin registry persistence.
- Discover bundled core plugins at startup.
- Add
/api/pluginsdiagnostics. - Add lifecycle states and health status.
Phase 2: Prompt and Surface Metadata
- Add prompt contribution registry.
- Add prompt contribution diagnostics.
- Add surface metadata registry.
- Represent existing Secrets CLI/MCP/dashboard/SDK/connectors in metadata.
Phase 3: Secrets Plugin Metadata
- Register
signet.secretsas bundled core plugin. - Associate existing secrets routes and surfaces with
signet.secrets. - Add Secrets prompt contribution.
- Add enable/disable behavior for prompt and advertised surfaces.
Phase 4: Local Provider Extraction
- Extract current local secret store behind provider interface.
- Preserve existing encryption and file format.
- Add compatibility fixtures for existing
secrets.enc. - Keep all existing secrets routes passing.
Phase 5: Guardrails and Docs
- Add audit events.
- Add docs/help metadata.
- Add CLI setup selection for bundled core plugins. Existing installs default
signet.secretsto enabled; new interactive installs explain Signet Secrets and ask whether to enable it. - Add degraded-mode tests.
- Update
docs/API.md,docs/SECRETS.md,docs/SDK.md,docs/MCP.md, and dashboard docs where behavior or ownership changed.
Validation and Tests
Required tests:
- manifest validation rejects invalid IDs, versions, missing docs metadata, and unsupported active runtimes.
- bundled
signet.secretsis discovered idempotently. /api/pluginslistssignet.secretswith expected state, capabilities, grants, and surfaces.- disabling
signet.secretsremoves its prompt contribution. - prompt diagnostics list active contributions with plugin provenance.
- prompt contribution clipping respects
maxTokens. - plugin health failure reports degraded state without crashing daemon.
- unsupported Rust sidecar manifest enters blocked state in V1.
- v1
secrets.encfixture remains readable by local provider. - startup does not rewrite existing
secrets.enc. - storing a new local secret writes the existing format.
- corrupt
secrets.encfails clearly and is not overwritten. - machine-mismatched
secrets.encfails clearly and is not overwritten. /api/secrets/*compatibility routes preserve existing behavior.execWithSecretsinjects resolved local values and redacts stdout/stderr.- ordinary API/MCP/dashboard/SDK responses do not include raw secret values.
- 1Password compatibility routes do not regress.
- setup registry tests prove new installs can persist
signet.secretsenabled or disabled without disturbing unrelated plugin registry entries.
Required local commands before PR:
bun test platform/daemon/src/secrets*.test.ts
bun test platform/daemon/src/plugin*.test.ts
bun run typecheck
bun run lint
The exact test filenames may differ, but the PR must include regression tests for the contracts above.
Documentation Updates
When implemented, update:
docs/API.mdfor plugin diagnostics routes and secrets ownership notes.docs/SECRETS.mdforsignet.secrets, local provider compatibility, and the no-raw-secret-read invariant.docs/SDK.mdto remove or correct any implication that ordinary SDK callers can retrieve raw secret values.docs/MCP.mdto state that secret tools use injection/listing only and are plugin-owned.docs/DASHBOARD.mdto describe plugin-owned Secrets settings and provider status.docs/specs/INDEX.mdanddocs/specs/dependencies.yamlwhen status changes.
Success Criteria
This spec is complete when:
signet.secretsappears as a bundled core plugin in daemon diagnostics.- Existing secrets routes, CLI, MCP, dashboard, and SDK behavior continue to work.
- Existing local
secrets.encfixtures pass without migration. - Secrets prompt contribution appears only when
signet.secretsis enabled. - Plugin registry and surface metadata are visible through diagnostics.
- Unsupported Rust/sidecar plugin metadata is blocked cleanly rather than executed or ignored silently.
- Tests prove secret values are not exposed through ordinary responses.
- Docs describe the plugin-owned Secrets architecture and compatibility guarantees.
- CLI setup enables
signet.secretsby default for existing installs, prompts new interactive installs in a Core plugins section, and supports non-interactive opt-out without deleting stored secrets.