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 everywhere yet,
signet.secrets
represented 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.enc
byte-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 .
- No dynamic dashboard panel rendering from arbitrary plugin code.
- No dynamic CLI command 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:
id
is stable and globally unique.version
is SemVer.publisher
is required.compatibility.signet
andcompatibility.pluginApi
are 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
. disabled
removes prompt contributions and active surface metadata.degraded
does 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
maxTokens
before 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 . 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 secret
CLI 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.enc
using 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
NAME
as 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 fails, the daemon should still be able to mount existing secrets routes through the local provider compatibility path.
- If
signet.secrets
is degraded, diagnostics must say whether local secrets are available, unavailable, or blocked by key mismatch/corruption. - If prompt contribution fails, prompt-submit continues without plugin contributions and records degraded diagnostics.
- Disabling
signet.secrets
removes 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/plugins
diagnostics. - 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.secrets
as 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.secrets
to 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.secrets
is discovered idempotently. /api/plugins
listssignet.secrets
with expected state, capabilities, grants, and surfaces.- disabling
signet.secrets
removes 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.enc
fixture remains readable by local provider. - startup does not rewrite existing
secrets.enc
. - storing a new local secret writes the existing format.
- corrupt
secrets.enc
fails clearly and is not overwritten. - machine-mismatched
secrets.enc
fails clearly and is not overwritten. /api/secrets/*
compatibility routes preserve existing behavior.execWithSecrets
injects 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.secrets
enabled 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.md
for plugin diagnostics routes and secrets ownership notes.docs/SECRETS.md
forsignet.secrets
, local provider compatibility, and the no-raw-secret-read invariant.docs/SDK.md
to remove or correct any implication that ordinary SDK callers can retrieve raw secret values.docs/MCP.md
to state that secret tools use injection/listing only and are plugin-owned.docs/DASHBOARD.md
to describe plugin-owned Secrets settings and provider status.docs/specs/INDEX.md
anddocs/specs/dependencies.yaml
when status changes.
Success Criteria #
This spec is complete when:
signet.secrets
appears as a bundled core plugin in daemon diagnostics.- Existing secrets routes, CLI, MCP, dashboard, and SDK behavior continue to work.
- Existing local
secrets.enc
fixtures pass without migration. - Secrets prompt contribution appears only when
signet.secrets
is 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.secrets
by default for existing installs, prompts new interactive installs in a Core plugins section, and supports non-interactive opt-out without deleting stored secrets.