Files
PaperClipAI/packages/plugins/sdk/src/host-client-factory.ts
T
Nicky LeachandPaperclip 1d571de5f9 Add shared native turn attribution boundaries (#18213)
## Thinking Path

> - Paperclip manages work performed by AI agents.
> - Native execution crosses the plugin worker, Runner, sidecar, and
durable event boundaries.
> - Existing outer spans leave repeated preparation and admission work
unattributed.
> - Optimizations need a common baseline with accurate ownership and
event meanings.
> - This change extends the existing opt-in tracing contract with
bounded timing receipts.
> - It preserves execution policy and keeps provider internals opaque
where the API provides no timing proof.

## Linked Issues or Issue Description

**What existing behavior does this improve?**
Attribution of native task preparation, retained/cold Runner handoff,
and completion-to-text work.

**Current behavior**
Worker spans use a flat provider contract. Workspace calls lack a
consistent dispatch envelope. Submitted-to-started timing is named as a
provider queue even though it includes other work. The transport does
not expose a shared breakdown of its waits.

**Proposed improvement**
Add invocation-scoped nesting, bounded numeric remote receipts,
current-run transport timing, durable receipt intervals, and distinct
completion/text milestones. Reuse the existing exporter. No latency
optimization is included.

Related public work: #17842 consolidates existing startup tracing tests;
it does not add this runtime coverage.

## What Changed

- Extend the optional SDK tracer with nested async scopes and completed
timing receipts. Keep the host responsible for parent identity,
capability checks, attributes, clocks, depth, and count limits.
- Add dispatch and bounded host-read timings for environment and
workspace providers.
- Resolve transport parents on each call so retained sessions do not
retain an earlier run's trace.
- Split cold/resume/attach/prepare/registration/command/started-event
waits. Add opt-in numeric ACPX Runner/sidecar status and prompt-start
receipts.
- Distinguish first completion request, validated tool receipt, final
result commit, first nonempty text, and event persistence. Rename the
submitted-to-started interval to match its meaning.
- Link completed earlier remote lifecycles to their host-owned observer
rather than placing them under a later polling request. Preserve
sampling, timestamp limits and budgets.
- Document the versioned coverage map, causal clock limits, and baseline
acceptance requirements.

## Verification

- Current head: GitHub CI is fully green. Standard Greptile and Apex are
both 5/5, with no unresolved review threads. Full-suite validation was
performed in CI; follow-up local validation was limited to affected
packages and tests.

- Review follow-up: early rejection, missing provider capability and
startup-parent regressions were reproduced and fixed. The affected
worker/SDK/timing set passes 128 tests; 28 timing/context tests, SDK
typecheck and direct server TypeScript checking pass. The server
typecheck wrapper reached an unrelated Runner rebuild and stopped
because Cargo was absent from PATH; full Runner/image validation remains
in CI.

- CI failure accounting: the earlier Turbo cache/force conflict and
Linux OpenCode shell assertion were reproduced and diagnosed. A direct
proc probe ruled out permissions; a portable shell comparison identified
Bash skipping BASH_ENV with stripped SHLVL and socket stdin. Upstream
#18230 now supplies the CI and ignored-stdin fixture fixes; this PR
adopts them without duplicate CI changes. The strict Linux OOM assertion
passed before this source-equivalent merge.
- Retry follow-up: the three insertion/replay/failure timing cases and
server typecheck pass. The existing database regression now asserts a
single marker across identical result replay; the current-head CI
integration gate passes.
- Upstream CI alignment: all three real-Turbo 2.11.10 integration cases
pass. The repository policy gate rejected committing a generated
lockfile; the final PR contains no lockfile edit and uses the maintained
inline stale-lockfile recovery.
- Linked-lifecycle follow-up: 44 focused host/export tests pass (three
optional SDK-dependent cases skip); SDK and server typechecks pass. The
API contract test verifies an explicit root context, host-owned observer
link and unsampled suppression.
- SDK typecheck and focused worker/invocation tests: 30 passed,
including clock changes, bounded legacy IPC, and manual-span
creation-scope parentage.
- Focused reviewed host/native/transaction-boundary checks: 44 passed.
The 86-test worker-manager suite passes after updating its exact
assertions for the host-owned timing budget found by CI. Earlier
preparation/first-text checks also passed.
- Affected server and Runner TypeScript checks pass after building local
workspace declarations.
- Merge follow-up: the profile/candidate startup subset passes 94 tests,
the four Grok source-identity checks pass, and Runner TypeScript
checking passes. A subsequent execution-factory merge passes 21 focused
timing/trace tests and server TypeScript checking.
- Pinned Rust build and the ACPX Rust subset pass. The hostile receipt
projection test passes. Source profile compatibility checks: 127 passed,
one existing skip. Grok identity checks: four passed.
- Broad transport/ACPX suite: 410 passed after building all fake
provider binaries. Earlier attempts failed on missing
Runner/fake-provider outputs; building the required fixtures resolved
those failures.
- The first CI run correctly rejected stale source attestations. After
integrating current master, Grok advances 13→14 and Copilot 84→85;
master’s historical fixtures and Pi 35 stay byte-identical. Provider
dependencies, models and permission defaults are unchanged. Identity
generation/checks now pass.
- The added real-Postgres result lock regression could not initialize
its local embedded cluster. The insertion/replay/failure unit
regressions pass. The current-head CI integration gate passes.
- Comprehensive repository checks and image builds are delegated to CI,
as requested for this instrumentation pass. No deployment, model run,
collector-ingestion proof, or latency improvement is claimed.

## Risks

The new SDK methods are optional. Legacy span calls and endpoint-off
behavior remain supported. The host accepts only closed timing names and
bounded attributes. Remote durations are anchored at local receipt;
transport/scheduling uncertainty remains explicit. Trace trees have
count/depth limits. The first rollout must inspect actual collector
exports before treating the baseline as accepted. Fresh live
qualification for the new source identities remains a rollout
requirement. Revert these commits to remove the new measurements; no
schema migration is required.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning and local code/tool
execution. The exact runtime model ID and context-window size are not
exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR following the enhancement
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal issue
details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Standard Greptile and Apex are both 5/5, with no unresolved
findings
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-29 17:45:56 -07:00

1127 lines
49 KiB
TypeScript

/**
* Host-side client factory — creates capability-gated handler maps for
* servicing worker→host JSON-RPC calls.
*
* When a plugin worker calls `ctx.state.get(...)` inside its process, the
* SDK serializes the call as a JSON-RPC request over stdio. On the host side,
* the `PluginWorkerManager` receives the request and dispatches it to the
* handler registered for that method. This module provides a factory that
* creates those handlers for all `WorkerToHostMethods`, with automatic
* capability enforcement.
*
* ## Design
*
* 1. **Capability gating**: Each handler checks the plugin's declared
* capabilities before executing. If the plugin lacks a required capability,
* the handler throws a `CapabilityDeniedError` (which the worker manager
* translates into a JSON-RPC error response with code
* `CAPABILITY_DENIED`).
*
* 2. **Service adapters**: The caller provides a `HostServices` object with
* concrete implementations of each platform service. The factory wires
* each handler to the appropriate service method.
*
* 3. **Type safety**: The returned handler map is typed as
* `WorkerToHostHandlers` (from `plugin-worker-manager.ts`) so it plugs
* directly into `WorkerStartOptions.hostHandlers`.
*
* @example
* ```ts
* const handlers = createHostClientHandlers({
* pluginId: "acme.linear",
* capabilities: manifest.capabilities,
* services: {
* config: { get: () => registry.getConfig(pluginId) },
* state: { get: ..., set: ..., delete: ... },
* entities: { upsert: ..., list: ... },
* // ... all services
* },
* });
*
* await workerManager.startWorker("acme.linear", {
* // ...
* hostHandlers: handlers,
* });
* ```
*
* @see PLUGIN_SPEC.md §13 — Host-Worker Protocol
* @see PLUGIN_SPEC.md §15 — Capability Model
*/
import type { PluginCapability } from "@paperclipai/shared";
import type { WorkerHostCallContext, WorkerToHostMethods, WorkerToHostMethodName } from "./protocol.js";
import { PLUGIN_RPC_ERROR_CODES } from "./protocol.js";
// ---------------------------------------------------------------------------
// Error types
// ---------------------------------------------------------------------------
/**
* Thrown when a plugin calls a host method it does not have the capability for.
*
* The `code` field is set to `PLUGIN_RPC_ERROR_CODES.CAPABILITY_DENIED` so
* the worker manager can propagate it as the correct JSON-RPC error code.
*/
export class CapabilityDeniedError extends Error {
override readonly name = "CapabilityDeniedError";
readonly code = PLUGIN_RPC_ERROR_CODES.CAPABILITY_DENIED;
constructor(pluginId: string, method: string, capability: PluginCapability) {
super(
`Plugin "${pluginId}" is missing required capability "${capability}" for method "${method}"`,
);
}
}
/**
* Thrown when a worker→host call asks for company-scoped data outside the
* company authorized for the current top-level plugin invocation.
*/
export class InvocationScopeDeniedError extends Error {
override readonly name = "InvocationScopeDeniedError";
readonly code = PLUGIN_RPC_ERROR_CODES.INVOCATION_SCOPE_DENIED;
constructor(pluginId: string, method: string, message: string) {
super(`Plugin "${pluginId}" is not allowed to perform "${method}": ${message}`);
}
}
// ---------------------------------------------------------------------------
// Host service interfaces
// ---------------------------------------------------------------------------
/**
* Service adapters that the host must provide. Each property maps to a group
* of `WorkerToHostMethods`. The factory wires JSON-RPC params to these
* function signatures.
*
* All methods return promises to support async I/O (database, HTTP, etc.).
*/
export interface HostServices {
/** Provides `config.get`. */
config: {
get(
params: WorkerToHostMethods["config.get"][0],
context?: WorkerHostCallContext,
): Promise<Record<string, unknown>>;
};
/** Provides trusted company-scoped local folder helpers. */
localFolders: {
declarations(params: WorkerToHostMethods["localFolders.declarations"][0]): Promise<WorkerToHostMethods["localFolders.declarations"][1]>;
configure(params: WorkerToHostMethods["localFolders.configure"][0]): Promise<WorkerToHostMethods["localFolders.configure"][1]>;
status(params: WorkerToHostMethods["localFolders.status"][0]): Promise<WorkerToHostMethods["localFolders.status"][1]>;
list(params: WorkerToHostMethods["localFolders.list"][0]): Promise<WorkerToHostMethods["localFolders.list"][1]>;
readText(params: WorkerToHostMethods["localFolders.readText"][0]): Promise<WorkerToHostMethods["localFolders.readText"][1]>;
writeTextAtomic(params: WorkerToHostMethods["localFolders.writeTextAtomic"][0]): Promise<WorkerToHostMethods["localFolders.writeTextAtomic"][1]>;
deleteFile(params: WorkerToHostMethods["localFolders.deleteFile"][0]): Promise<WorkerToHostMethods["localFolders.deleteFile"][1]>;
};
/** Provides `state.get`, `state.set`, `state.delete`. */
state: {
get(params: WorkerToHostMethods["state.get"][0]): Promise<WorkerToHostMethods["state.get"][1]>;
set(params: WorkerToHostMethods["state.set"][0]): Promise<void>;
delete(params: WorkerToHostMethods["state.delete"][0]): Promise<void>;
};
/** Provides restricted plugin database namespace methods. */
db: {
namespace(params: WorkerToHostMethods["db.namespace"][0]): Promise<WorkerToHostMethods["db.namespace"][1]>;
query(params: WorkerToHostMethods["db.query"][0]): Promise<WorkerToHostMethods["db.query"][1]>;
execute(params: WorkerToHostMethods["db.execute"][0]): Promise<WorkerToHostMethods["db.execute"][1]>;
};
/** Provides `entities.upsert`, `entities.list`. */
entities: {
upsert(params: WorkerToHostMethods["entities.upsert"][0]): Promise<WorkerToHostMethods["entities.upsert"][1]>;
list(params: WorkerToHostMethods["entities.list"][0]): Promise<WorkerToHostMethods["entities.list"][1]>;
};
/** Provides `events.emit` and `events.subscribe`. */
events: {
listLifecycle(params: WorkerToHostMethods["events.listLifecycle"][0]): Promise<WorkerToHostMethods["events.listLifecycle"][1]>;
acknowledgeLifecycle(params: WorkerToHostMethods["events.acknowledgeLifecycle"][0]): Promise<void>;
emit(params: WorkerToHostMethods["events.emit"][0]): Promise<void>;
subscribe(params: WorkerToHostMethods["events.subscribe"][0]): Promise<void>;
};
/** Provides `http.fetch`. */
http: {
fetch(params: WorkerToHostMethods["http.fetch"][0]): Promise<WorkerToHostMethods["http.fetch"][1]>;
};
/** Provides `secrets.resolve`. */
secrets: {
resolve(
params: WorkerToHostMethods["secrets.resolve"][0],
context?: WorkerHostCallContext,
): Promise<string>;
};
/** Provides `activity.log`. */
activity: {
log(params: {
companyId: string;
message: string;
entityType?: string;
entityId?: string;
metadata?: Record<string, unknown>;
}): Promise<void>;
};
/** Provides `metrics.write`. */
metrics: {
write(params: WorkerToHostMethods["metrics.write"][0]): Promise<void>;
};
/** Provides `telemetry.track`. */
telemetry: {
track(params: WorkerToHostMethods["telemetry.track"][0]): Promise<void>;
};
/** Provides `log`. */
logger: {
log(params: WorkerToHostMethods["log"][0]): Promise<void>;
};
/** Provides `span.record`. The context carries the host-minted `traceparent`. */
tracer: {
record(
params: WorkerToHostMethods["span.record"][0],
context?: WorkerHostCallContext,
): Promise<void>;
};
/** Provides `companies.list`, `companies.get`. */
companies: {
list(params: WorkerToHostMethods["companies.list"][0]): Promise<WorkerToHostMethods["companies.list"][1]>;
get(params: WorkerToHostMethods["companies.get"][0]): Promise<WorkerToHostMethods["companies.get"][1]>;
};
/** Provides `projects.list`, `projects.get`, `projects.listWorkspaces`, `projects.getPrimaryWorkspace`, `projects.getWorkspaceForIssue`. */
projects: {
list(params: WorkerToHostMethods["projects.list"][0]): Promise<WorkerToHostMethods["projects.list"][1]>;
get(params: WorkerToHostMethods["projects.get"][0]): Promise<WorkerToHostMethods["projects.get"][1]>;
resolveGitCredential(params: WorkerToHostMethods["projects.resolveGitCredential"][0], context: WorkerHostCallContext): Promise<string | null>;
getInstanceSecrets(params: WorkerToHostMethods["projects.getInstanceSecrets"][0]): Promise<WorkerToHostMethods["projects.getInstanceSecrets"][1]>;
listWorkspaces(params: WorkerToHostMethods["projects.listWorkspaces"][0]): Promise<WorkerToHostMethods["projects.listWorkspaces"][1]>;
getPrimaryWorkspace(params: WorkerToHostMethods["projects.getPrimaryWorkspace"][0]): Promise<WorkerToHostMethods["projects.getPrimaryWorkspace"][1]>;
getWorkspaceForIssue(params: WorkerToHostMethods["projects.getWorkspaceForIssue"][0]): Promise<WorkerToHostMethods["projects.getWorkspaceForIssue"][1]>;
getManaged(params: WorkerToHostMethods["projects.managed.get"][0]): Promise<WorkerToHostMethods["projects.managed.get"][1]>;
reconcileManaged(params: WorkerToHostMethods["projects.managed.reconcile"][0]): Promise<WorkerToHostMethods["projects.managed.reconcile"][1]>;
resetManaged(params: WorkerToHostMethods["projects.managed.reset"][0]): Promise<WorkerToHostMethods["projects.managed.reset"][1]>;
};
/** Provides `executionWorkspaces.get`. */
executionWorkspaces: {
get(params: WorkerToHostMethods["executionWorkspaces.get"][0]): Promise<WorkerToHostMethods["executionWorkspaces.get"][1]>;
};
/** Provides `routines.managed.*`. */
routines: {
managedGet(params: WorkerToHostMethods["routines.managed.get"][0]): Promise<WorkerToHostMethods["routines.managed.get"][1]>;
managedReconcile(params: WorkerToHostMethods["routines.managed.reconcile"][0]): Promise<WorkerToHostMethods["routines.managed.reconcile"][1]>;
managedReset(params: WorkerToHostMethods["routines.managed.reset"][0]): Promise<WorkerToHostMethods["routines.managed.reset"][1]>;
managedUpdate(params: WorkerToHostMethods["routines.managed.update"][0]): Promise<WorkerToHostMethods["routines.managed.update"][1]>;
managedRun(params: WorkerToHostMethods["routines.managed.run"][0]): Promise<WorkerToHostMethods["routines.managed.run"][1]>;
};
/** Provides `skills.managed.*`. */
skills: {
managedGet(params: WorkerToHostMethods["skills.managed.get"][0]): Promise<WorkerToHostMethods["skills.managed.get"][1]>;
managedReconcile(params: WorkerToHostMethods["skills.managed.reconcile"][0]): Promise<WorkerToHostMethods["skills.managed.reconcile"][1]>;
managedReset(params: WorkerToHostMethods["skills.managed.reset"][0]): Promise<WorkerToHostMethods["skills.managed.reset"][1]>;
};
/** Provides issue read/write, relation, checkout, wakeup, summary, comment methods. */
issues: {
list(params: WorkerToHostMethods["issues.list"][0]): Promise<WorkerToHostMethods["issues.list"][1]>;
get(params: WorkerToHostMethods["issues.get"][0]): Promise<WorkerToHostMethods["issues.get"][1]>;
create(params: WorkerToHostMethods["issues.create"][0]): Promise<WorkerToHostMethods["issues.create"][1]>;
update(params: WorkerToHostMethods["issues.update"][0]): Promise<WorkerToHostMethods["issues.update"][1]>;
getRelations(params: WorkerToHostMethods["issues.relations.get"][0]): Promise<WorkerToHostMethods["issues.relations.get"][1]>;
setBlockedBy(params: WorkerToHostMethods["issues.relations.setBlockedBy"][0]): Promise<WorkerToHostMethods["issues.relations.setBlockedBy"][1]>;
addBlockers(params: WorkerToHostMethods["issues.relations.addBlockers"][0]): Promise<WorkerToHostMethods["issues.relations.addBlockers"][1]>;
removeBlockers(params: WorkerToHostMethods["issues.relations.removeBlockers"][0]): Promise<WorkerToHostMethods["issues.relations.removeBlockers"][1]>;
assertCheckoutOwner(params: WorkerToHostMethods["issues.assertCheckoutOwner"][0]): Promise<WorkerToHostMethods["issues.assertCheckoutOwner"][1]>;
getSubtree(params: WorkerToHostMethods["issues.getSubtree"][0]): Promise<WorkerToHostMethods["issues.getSubtree"][1]>;
requestWakeup(params: WorkerToHostMethods["issues.requestWakeup"][0]): Promise<WorkerToHostMethods["issues.requestWakeup"][1]>;
requestWakeups(params: WorkerToHostMethods["issues.requestWakeups"][0]): Promise<WorkerToHostMethods["issues.requestWakeups"][1]>;
getOrchestrationSummary(params: WorkerToHostMethods["issues.summaries.getOrchestration"][0]): Promise<WorkerToHostMethods["issues.summaries.getOrchestration"][1]>;
listComments(params: WorkerToHostMethods["issues.listComments"][0]): Promise<WorkerToHostMethods["issues.listComments"][1]>;
createComment(params: WorkerToHostMethods["issues.createComment"][0]): Promise<WorkerToHostMethods["issues.createComment"][1]>;
createInteraction(params: WorkerToHostMethods["issues.createInteraction"][0]): Promise<WorkerToHostMethods["issues.createInteraction"][1]>;
listInteractions(params: WorkerToHostMethods["issues.listInteractions"][0]): Promise<WorkerToHostMethods["issues.listInteractions"][1]>;
respondInteraction(params: WorkerToHostMethods["issues.respondInteraction"][0]): Promise<WorkerToHostMethods["issues.respondInteraction"][1]>;
listAttachments(params: WorkerToHostMethods["issues.listAttachments"][0]): Promise<WorkerToHostMethods["issues.listAttachments"][1]>;
getAttachmentContent(params: WorkerToHostMethods["issues.getAttachmentContent"][0]): Promise<WorkerToHostMethods["issues.getAttachmentContent"][1]>;
};
/** Provides `approvals.list`, `approvals.get`, `approvals.decide`. */
approvals: {
list(params: WorkerToHostMethods["approvals.list"][0]): Promise<WorkerToHostMethods["approvals.list"][1]>;
get(params: WorkerToHostMethods["approvals.get"][0]): Promise<WorkerToHostMethods["approvals.get"][1]>;
decide(params: WorkerToHostMethods["approvals.decide"][0]): Promise<WorkerToHostMethods["approvals.decide"][1]>;
};
/** Provides `issues.documents.list`, `issues.documents.get`, `issues.documents.upsert`, `issues.documents.delete`. */
issueDocuments: {
list(params: WorkerToHostMethods["issues.documents.list"][0]): Promise<WorkerToHostMethods["issues.documents.list"][1]>;
get(params: WorkerToHostMethods["issues.documents.get"][0]): Promise<WorkerToHostMethods["issues.documents.get"][1]>;
upsert(params: WorkerToHostMethods["issues.documents.upsert"][0]): Promise<WorkerToHostMethods["issues.documents.upsert"][1]>;
delete(params: WorkerToHostMethods["issues.documents.delete"][0]): Promise<WorkerToHostMethods["issues.documents.delete"][1]>;
};
/** Provides `agents.list`, `agents.get`, `agents.pause`, `agents.resume`, `agents.invoke`. */
agents: {
list(params: WorkerToHostMethods["agents.list"][0]): Promise<WorkerToHostMethods["agents.list"][1]>;
getRuntimeIdentity?(params: WorkerToHostMethods["agents.getRuntimeIdentity"][0]): Promise<WorkerToHostMethods["agents.getRuntimeIdentity"][1]>;
getRuntime?(params: WorkerToHostMethods["agents.getRuntime"][0]): Promise<WorkerToHostMethods["agents.getRuntime"][1]>;
get(params: WorkerToHostMethods["agents.get"][0]): Promise<WorkerToHostMethods["agents.get"][1]>;
pause(params: WorkerToHostMethods["agents.pause"][0]): Promise<WorkerToHostMethods["agents.pause"][1]>;
resume(params: WorkerToHostMethods["agents.resume"][0]): Promise<WorkerToHostMethods["agents.resume"][1]>;
invoke(params: WorkerToHostMethods["agents.invoke"][0]): Promise<WorkerToHostMethods["agents.invoke"][1]>;
managedGet(params: WorkerToHostMethods["agents.managed.get"][0]): Promise<WorkerToHostMethods["agents.managed.get"][1]>;
managedReconcile(params: WorkerToHostMethods["agents.managed.reconcile"][0]): Promise<WorkerToHostMethods["agents.managed.reconcile"][1]>;
managedReset(params: WorkerToHostMethods["agents.managed.reset"][0]): Promise<WorkerToHostMethods["agents.managed.reset"][1]>;
};
/** Provides `agents.sessions.create`, `agents.sessions.list`, `agents.sessions.sendMessage`, `agents.sessions.close`. */
agentSessions: {
create(params: WorkerToHostMethods["agents.sessions.create"][0]): Promise<WorkerToHostMethods["agents.sessions.create"][1]>;
list(params: WorkerToHostMethods["agents.sessions.list"][0]): Promise<WorkerToHostMethods["agents.sessions.list"][1]>;
sendMessage(params: WorkerToHostMethods["agents.sessions.sendMessage"][0]): Promise<WorkerToHostMethods["agents.sessions.sendMessage"][1]>;
close(params: WorkerToHostMethods["agents.sessions.close"][0]): Promise<void>;
};
/** Provides `goals.list`, `goals.get`, `goals.create`, `goals.update`. */
goals: {
list(params: WorkerToHostMethods["goals.list"][0]): Promise<WorkerToHostMethods["goals.list"][1]>;
get(params: WorkerToHostMethods["goals.get"][0]): Promise<WorkerToHostMethods["goals.get"][1]>;
create(params: WorkerToHostMethods["goals.create"][0]): Promise<WorkerToHostMethods["goals.create"][1]>;
update(params: WorkerToHostMethods["goals.update"][0]): Promise<WorkerToHostMethods["goals.update"][1]>;
};
/** Provides `access.members.*` and `access.invites.*`. */
access: {
listMembers(params: WorkerToHostMethods["access.members.list"][0]): Promise<WorkerToHostMethods["access.members.list"][1]>;
getMember(params: WorkerToHostMethods["access.members.get"][0]): Promise<WorkerToHostMethods["access.members.get"][1]>;
updateMember(params: WorkerToHostMethods["access.members.update"][0]): Promise<WorkerToHostMethods["access.members.update"][1]>;
listInvites(params: WorkerToHostMethods["access.invites.list"][0]): Promise<WorkerToHostMethods["access.invites.list"][1]>;
createInvite(params: WorkerToHostMethods["access.invites.create"][0]): Promise<WorkerToHostMethods["access.invites.create"][1]>;
revokeInvite(params: WorkerToHostMethods["access.invites.revoke"][0]): Promise<WorkerToHostMethods["access.invites.revoke"][1]>;
};
/** Provides authorization grant, policy, preview, and audit helpers. */
authorization: {
listGrants(params: WorkerToHostMethods["authorization.grants.list"][0]): Promise<WorkerToHostMethods["authorization.grants.list"][1]>;
setGrants(params: WorkerToHostMethods["authorization.grants.set"][0]): Promise<WorkerToHostMethods["authorization.grants.set"][1]>;
policySummary(params: WorkerToHostMethods["authorization.policies.summary"][0]): Promise<WorkerToHostMethods["authorization.policies.summary"][1]>;
getPolicy(params: WorkerToHostMethods["authorization.policies.get"][0]): Promise<WorkerToHostMethods["authorization.policies.get"][1]>;
updatePolicy(params: WorkerToHostMethods["authorization.policies.update"][0]): Promise<WorkerToHostMethods["authorization.policies.update"][1]>;
previewAssignment(params: WorkerToHostMethods["authorization.policies.previewAssignment"][0]): Promise<WorkerToHostMethods["authorization.policies.previewAssignment"][1]>;
explainAssignment(params: WorkerToHostMethods["authorization.policies.explainAssignment"][0]): Promise<WorkerToHostMethods["authorization.policies.explainAssignment"][1]>;
searchAudit(params: WorkerToHostMethods["authorization.audit.search"][0]): Promise<WorkerToHostMethods["authorization.audit.search"][1]>;
};
}
// ---------------------------------------------------------------------------
// Factory input
// ---------------------------------------------------------------------------
/**
* Options for `createHostClientHandlers`.
*/
export interface HostClientFactoryOptions {
/** The plugin ID. Used for error messages and logging. */
pluginId: string;
/**
* The capabilities declared by the plugin in its manifest. The factory
* enforces these at runtime before delegating to the service adapter.
*/
capabilities: readonly PluginCapability[];
/**
* Concrete implementations of host platform services. Each handler in the
* returned map delegates to the corresponding service method.
*/
services: HostServices;
}
// ---------------------------------------------------------------------------
// Handler map type (compatible with WorkerToHostHandlers from worker manager)
// ---------------------------------------------------------------------------
/**
* A handler function for a specific worker→host method.
*/
type HostHandler<M extends WorkerToHostMethodName> = (
params: WorkerToHostMethods[M][0],
context?: WorkerHostCallContext,
) => Promise<WorkerToHostMethods[M][1]>;
/**
* A complete map of all worker→host method handlers.
*
* This type matches `WorkerToHostHandlers` from `plugin-worker-manager.ts`
* but makes every handler required (the factory always provides all handlers).
*/
export type HostClientHandlers = {
[M in WorkerToHostMethodName]: HostHandler<M>;
};
// ---------------------------------------------------------------------------
// Capability → method mapping
// ---------------------------------------------------------------------------
/**
* Maps each worker→host RPC method to the capability required to invoke it.
* Methods without a capability requirement (e.g. `config.get`, `log`) are
* mapped to `null`.
*
* @see PLUGIN_SPEC.md §15 — Capability Model
*/
const METHOD_CAPABILITY_MAP: Record<WorkerToHostMethodName, PluginCapability | null> = {
// Config — always allowed
"config.get": null,
// Trusted local folders
"localFolders.declarations": null,
"localFolders.configure": "local.folders",
"localFolders.status": "local.folders",
"localFolders.list": "local.folders",
"localFolders.readText": "local.folders",
"localFolders.writeTextAtomic": "local.folders",
"localFolders.deleteFile": "local.folders",
// State
"state.get": "plugin.state.read",
"state.set": "plugin.state.write",
"state.delete": "plugin.state.write",
"db.namespace": "database.namespace.read",
"db.query": "database.namespace.read",
"db.execute": "database.namespace.write",
// Entities — no specific capability required (plugin-scoped by design)
"entities.upsert": null,
"entities.list": null,
// Events
"events.emit": "events.emit",
"events.subscribe": "events.subscribe",
"events.listLifecycle": "events.subscribe",
"events.acknowledgeLifecycle": "events.subscribe",
// HTTP
"http.fetch": "http.outbound",
// Secrets
"secrets.resolve": "secrets.read-ref",
// Activity
"activity.log": "activity.log.write",
// Metrics
"metrics.write": "metrics.write",
// Telemetry
"telemetry.track": "telemetry.track",
// Logger — always allowed
"log": null,
// Timing sink for environment drivers or workspace providers. Parentage
// still derives solely from the current host-owned invocation.
"span.record": "environment.drivers.register",
// Companies
"companies.list": "companies.read",
"companies.get": "companies.read",
// Projects
"projects.list": "projects.read",
"projects.get": "projects.read",
"projects.getInstanceSecrets": "projects.instance-secrets",
"projects.resolveGitCredential": "projects.git-credentials",
"projects.listWorkspaces": "project.workspaces.read",
"projects.getPrimaryWorkspace": "project.workspaces.read",
"projects.getWorkspaceForIssue": "project.workspaces.read",
"executionWorkspaces.get": "execution.workspaces.read",
"projects.managed.get": "projects.managed",
"projects.managed.reconcile": "projects.managed",
"projects.managed.reset": "projects.managed",
"routines.managed.get": "routines.managed",
"routines.managed.reconcile": "routines.managed",
"routines.managed.reset": "routines.managed",
"routines.managed.update": "routines.managed",
"routines.managed.run": "routines.managed",
"skills.managed.get": "skills.managed",
"skills.managed.reconcile": "skills.managed",
"skills.managed.reset": "skills.managed",
// Issues
"issues.list": "issues.read",
"issues.get": "issues.read",
"issues.create": "issues.create",
"issues.update": "issues.update",
"issues.relations.get": "issue.relations.read",
"issues.relations.setBlockedBy": "issue.relations.write",
"issues.relations.addBlockers": "issue.relations.write",
"issues.relations.removeBlockers": "issue.relations.write",
"issues.assertCheckoutOwner": "issues.checkout",
"issues.getSubtree": "issue.subtree.read",
"issues.requestWakeup": "issues.wakeup",
"issues.requestWakeups": "issues.wakeup",
"issues.summaries.getOrchestration": "issues.orchestration.read",
"issues.listComments": "issue.comments.read",
"issues.createComment": "issue.comments.create",
"issues.createInteraction": "issue.interactions.create",
"issues.listInteractions": "issue.interactions.read",
"issues.respondInteraction": "issue.interactions.respond",
"issues.listAttachments": "issue.attachments.read",
"issues.getAttachmentContent": "issue.attachments.read",
// Approvals
"approvals.list": "approvals.read",
"approvals.get": "approvals.read",
"approvals.decide": "approvals.respond",
// Issue Documents
"issues.documents.list": "issue.documents.read",
"issues.documents.get": "issue.documents.read",
"issues.documents.upsert": "issue.documents.write",
"issues.documents.delete": "issue.documents.write",
// Agents
"agents.list": "agents.read",
"agents.get": "agents.read",
"agents.getRuntime": "agents.lifecycle.manage",
"agents.getRuntimeIdentity": "agents.lifecycle.manage",
"agents.pause": "agents.pause",
"agents.resume": "agents.resume",
"agents.invoke": "agents.invoke",
"agents.managed.get": "agents.managed",
"agents.managed.reconcile": "agents.managed",
"agents.managed.reset": "agents.managed",
// Agent Sessions
"agents.sessions.create": "agent.sessions.create",
"agents.sessions.list": "agent.sessions.list",
"agents.sessions.sendMessage": "agent.sessions.send",
"agents.sessions.close": "agent.sessions.close",
// Goals
"goals.list": "goals.read",
"goals.get": "goals.read",
"goals.create": "goals.create",
"goals.update": "goals.update",
// Access
"access.members.list": "access.members.read",
"access.members.get": "access.members.read",
"access.members.update": "access.members.write",
"access.invites.list": "access.invites.read",
"access.invites.create": "access.invites.write",
"access.invites.revoke": "access.invites.write",
// Authorization
"authorization.grants.list": "authorization.grants.read",
"authorization.grants.set": "authorization.grants.write",
"authorization.policies.summary": "authorization.policies.read",
"authorization.policies.get": "authorization.policies.read",
"authorization.policies.update": "authorization.policies.write",
"authorization.policies.previewAssignment": "authorization.policies.read",
"authorization.policies.explainAssignment": "authorization.policies.read",
"authorization.audit.search": "authorization.audit.read",
};
// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------
/**
* Create a complete handler map for all worker→host JSON-RPC methods.
*
* Each handler:
* 1. Checks the plugin's declared capabilities against the required capability
* for the method (if any).
* 2. Delegates to the corresponding service adapter method.
* 3. Returns the service result, which is serialized as the JSON-RPC response
* by the worker manager.
*
* If a capability check fails, the handler throws a `CapabilityDeniedError`
* with code `CAPABILITY_DENIED`. The worker manager catches this and sends a
* JSON-RPC error response to the worker, which surfaces as a `JsonRpcCallError`
* in the plugin's SDK client.
*
* @param options - Plugin ID, capabilities, and service adapters
* @returns A handler map suitable for `WorkerStartOptions.hostHandlers`
*/
export function createHostClientHandlers(
options: HostClientFactoryOptions,
): HostClientHandlers {
const { pluginId, services } = options;
const capabilitySet = new Set<PluginCapability>(options.capabilities);
type CompanyScopeRequest =
| { kind: "none" }
| { kind: "single"; companyId: string }
| { kind: "all" };
const noCompanyScope: CompanyScopeRequest = { kind: "none" };
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readNonEmptyString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function requestedCompanyScope(
method: WorkerToHostMethodName,
params: unknown,
): CompanyScopeRequest {
if (method === "companies.list") return { kind: "all" };
if (!isRecord(params)) return noCompanyScope;
const companyId = readNonEmptyString(params.companyId);
if (companyId) return { kind: "single", companyId };
if (params.scopeKind === "company") {
const scopeId = readNonEmptyString(params.scopeId);
return scopeId ? { kind: "single", companyId: scopeId } : { kind: "all" };
}
if (method === "events.subscribe" && isRecord(params.filter)) {
const filterCompanyId = readNonEmptyString(params.filter.companyId);
if (filterCompanyId) return { kind: "single", companyId: filterCompanyId };
}
return noCompanyScope;
}
function requireInvocationCompanyScope(
method: WorkerToHostMethodName,
params: unknown,
context?: WorkerHostCallContext,
): void {
const requested = requestedCompanyScope(method, params);
if (requested.kind === "none") return;
if (context?.invalidInvocationScope) {
throw new InvocationScopeDeniedError(
pluginId,
method,
"the worker referenced a missing, expired, or unknown invocation scope",
);
}
const allowedCompanyId = readNonEmptyString(context?.invocationScope?.companyId);
if (requested.kind === "all") {
if (method === "companies.list") return;
if (!allowedCompanyId) {
throw new InvocationScopeDeniedError(pluginId, method, "company context is required");
}
throw new InvocationScopeDeniedError(
pluginId,
method,
`the current invocation is scoped to company "${allowedCompanyId}"`,
);
}
if (!allowedCompanyId) {
throw new InvocationScopeDeniedError(pluginId, method, "company context is required");
}
if (requested.companyId !== allowedCompanyId) {
throw new InvocationScopeDeniedError(
pluginId,
method,
`requested company "${requested.companyId}" but the current invocation is scoped to company "${allowedCompanyId}"`,
);
}
}
function resolveRequiredCompanyId(
method: WorkerToHostMethodName,
params: unknown,
context?: WorkerHostCallContext,
): string {
if (context?.invalidInvocationScope) {
throw new InvocationScopeDeniedError(
pluginId,
method,
"the worker referenced a missing, expired, or unknown invocation scope",
);
}
const requested = requestedCompanyScope(method, params);
const scopedCompanyId = readNonEmptyString(context?.invocationScope?.companyId);
if (requested.kind === "single") {
if (!scopedCompanyId) {
throw new InvocationScopeDeniedError(pluginId, method, "company context is required");
}
if (requested.companyId !== scopedCompanyId) {
throw new InvocationScopeDeniedError(
pluginId,
method,
`requested company "${requested.companyId}" but the current invocation is scoped to company "${scopedCompanyId}"`,
);
}
return scopedCompanyId;
}
if (scopedCompanyId) return scopedCompanyId;
throw new InvocationScopeDeniedError(pluginId, method, "company context is required");
}
/**
* Assert that the plugin has the required capability for a method.
* Throws `CapabilityDeniedError` if the capability is missing.
*/
function requireCapability(
method: WorkerToHostMethodName,
): void {
const required = METHOD_CAPABILITY_MAP[method];
if (required === null) return; // No capability required
if (capabilitySet.has(required)) return;
if (method === "span.record" && capabilitySet.has("workspaces.manage")) return;
throw new CapabilityDeniedError(pluginId, method, required);
}
/**
* Create a capability-gated proxy handler for a method.
*
* @param method - The RPC method name (used for capability lookup)
* @param handler - The actual handler implementation
* @returns A wrapper that checks capabilities before delegating
*/
function gated<M extends WorkerToHostMethodName>(
method: M,
handler: HostHandler<M>,
): HostHandler<M> {
return async (params: WorkerToHostMethods[M][0], context?: WorkerHostCallContext) => {
requireCapability(method);
requireInvocationCompanyScope(method, params, context);
return handler(params, context);
};
}
// -------------------------------------------------------------------------
// Build the complete handler map
// -------------------------------------------------------------------------
return {
// Config
"config.get": gated("config.get", async (params, context) => {
const companyId = resolveRequiredCompanyId("config.get", params, context);
return services.config.get({ ...params, companyId }, context);
}),
"localFolders.declarations": gated("localFolders.declarations", async (params) => {
return services.localFolders.declarations(params);
}),
"localFolders.configure": gated("localFolders.configure", async (params) => {
return services.localFolders.configure(params);
}),
"localFolders.status": gated("localFolders.status", async (params) => {
return services.localFolders.status(params);
}),
"localFolders.list": gated("localFolders.list", async (params) => {
return services.localFolders.list(params);
}),
"localFolders.readText": gated("localFolders.readText", async (params) => {
return services.localFolders.readText(params);
}),
"localFolders.writeTextAtomic": gated("localFolders.writeTextAtomic", async (params) => {
return services.localFolders.writeTextAtomic(params);
}),
"localFolders.deleteFile": gated("localFolders.deleteFile", async (params) => {
return services.localFolders.deleteFile(params);
}),
// State
"state.get": gated("state.get", async (params) => {
return services.state.get(params);
}),
"state.set": gated("state.set", async (params) => {
return services.state.set(params);
}),
"state.delete": gated("state.delete", async (params) => {
return services.state.delete(params);
}),
"db.namespace": gated("db.namespace", async (params) => {
return services.db.namespace(params);
}),
"db.query": gated("db.query", async (params) => {
return services.db.query(params);
}),
"db.execute": gated("db.execute", async (params) => {
return services.db.execute(params);
}),
// Entities
"entities.upsert": gated("entities.upsert", async (params) => {
return services.entities.upsert(params);
}),
"entities.list": gated("entities.list", async (params) => {
return services.entities.list(params);
}),
// Events
"events.listLifecycle": gated("events.listLifecycle", async (params) => services.events.listLifecycle(params)),
"events.acknowledgeLifecycle": gated("events.acknowledgeLifecycle", async (params) => services.events.acknowledgeLifecycle(params)),
"events.emit": gated("events.emit", async (params) => {
return services.events.emit(params);
}),
"events.subscribe": gated("events.subscribe", async (params) => {
return services.events.subscribe(params);
}),
// HTTP
"http.fetch": gated("http.fetch", async (params) => {
return services.http.fetch(params);
}),
// Secrets
"secrets.resolve": gated("secrets.resolve", async (params, context) => {
const companyId = resolveRequiredCompanyId("secrets.resolve", params, context);
return services.secrets.resolve({ ...params, companyId }, context);
}),
// Activity
"activity.log": gated("activity.log", async (params) => {
return services.activity.log(params);
}),
// Metrics
"metrics.write": gated("metrics.write", async (params) => {
return services.metrics.write(params);
}),
// Telemetry
"telemetry.track": gated("telemetry.track", async (params) => {
return services.telemetry.track(params);
}),
// Logger
"log": gated("log", async (params) => {
return services.logger.log(params);
}),
// Provider span sink. The context carries the host-minted `traceparent`.
"span.record": gated("span.record", async (params, context) => {
return services.tracer.record(params, context);
}),
// Companies
"companies.list": gated("companies.list", async (params, context) => {
const rows = await services.companies.list(params);
const allowedCompanyId = readNonEmptyString(context?.invocationScope?.companyId);
if (!allowedCompanyId) return rows;
return rows.filter((company) =>
isRecord(company) && company.id === allowedCompanyId,
) as WorkerToHostMethods["companies.list"][1];
}),
"companies.get": gated("companies.get", async (params) => {
return services.companies.get(params);
}),
// Projects
"projects.list": gated("projects.list", async (params) => {
return services.projects.list(params);
}),
"projects.resolveGitCredential": gated("projects.resolveGitCredential", async (params, context) => {
const companyId = resolveRequiredCompanyId("projects.resolveGitCredential", params, context);
const scope = context?.invocationScope?.projectGit;
if (!scope || scope.projectId !== params.projectId || (scope.workspaceId && scope.workspaceId !== params.workspaceId)) {
throw new Error("Git credentials require a matching host project operation");
}
return services.projects.resolveGitCredential({ ...params, companyId }, context!);
}),
"projects.getInstanceSecrets": gated("projects.getInstanceSecrets", async (params, context) => {
const companyId = resolveRequiredCompanyId("projects.getInstanceSecrets", params, context);
return services.projects.getInstanceSecrets({ ...params, companyId });
}),
"projects.get": gated("projects.get", async (params) => {
return services.projects.get(params);
}),
"projects.listWorkspaces": gated("projects.listWorkspaces", async (params) => {
return services.projects.listWorkspaces(params);
}),
"projects.getPrimaryWorkspace": gated("projects.getPrimaryWorkspace", async (params) => {
return services.projects.getPrimaryWorkspace(params);
}),
"projects.getWorkspaceForIssue": gated("projects.getWorkspaceForIssue", async (params) => {
return services.projects.getWorkspaceForIssue(params);
}),
"executionWorkspaces.get": gated("executionWorkspaces.get", async (params) => {
return services.executionWorkspaces.get(params);
}),
"projects.managed.get": gated("projects.managed.get", async (params) => {
return services.projects.getManaged(params);
}),
"projects.managed.reconcile": gated("projects.managed.reconcile", async (params) => {
return services.projects.reconcileManaged(params);
}),
"projects.managed.reset": gated("projects.managed.reset", async (params) => {
return services.projects.resetManaged(params);
}),
// Routines
"routines.managed.get": gated("routines.managed.get", async (params) => {
return services.routines.managedGet(params);
}),
"routines.managed.reconcile": gated("routines.managed.reconcile", async (params) => {
return services.routines.managedReconcile(params);
}),
"routines.managed.reset": gated("routines.managed.reset", async (params) => {
return services.routines.managedReset(params);
}),
"routines.managed.update": gated("routines.managed.update", async (params) => {
return services.routines.managedUpdate(params);
}),
"routines.managed.run": gated("routines.managed.run", async (params) => {
return services.routines.managedRun(params);
}),
// Skills
"skills.managed.get": gated("skills.managed.get", async (params) => {
return services.skills.managedGet(params);
}),
"skills.managed.reconcile": gated("skills.managed.reconcile", async (params) => {
return services.skills.managedReconcile(params);
}),
"skills.managed.reset": gated("skills.managed.reset", async (params) => {
return services.skills.managedReset(params);
}),
// Issues
"issues.list": gated("issues.list", async (params) => {
return services.issues.list(params);
}),
"issues.get": gated("issues.get", async (params) => {
return services.issues.get(params);
}),
"issues.create": gated("issues.create", async (params) => {
return services.issues.create(params);
}),
"issues.update": gated("issues.update", async (params) => {
return services.issues.update(params);
}),
"issues.relations.get": gated("issues.relations.get", async (params) => {
return services.issues.getRelations(params);
}),
"issues.relations.setBlockedBy": gated("issues.relations.setBlockedBy", async (params) => {
return services.issues.setBlockedBy(params);
}),
"issues.relations.addBlockers": gated("issues.relations.addBlockers", async (params) => {
return services.issues.addBlockers(params);
}),
"issues.relations.removeBlockers": gated("issues.relations.removeBlockers", async (params) => {
return services.issues.removeBlockers(params);
}),
"issues.assertCheckoutOwner": gated("issues.assertCheckoutOwner", async (params) => {
return services.issues.assertCheckoutOwner(params);
}),
"issues.getSubtree": gated("issues.getSubtree", async (params) => {
return services.issues.getSubtree(params);
}),
"issues.requestWakeup": gated("issues.requestWakeup", async (params) => {
return services.issues.requestWakeup(params);
}),
"issues.requestWakeups": gated("issues.requestWakeups", async (params) => {
return services.issues.requestWakeups(params);
}),
"issues.summaries.getOrchestration": gated("issues.summaries.getOrchestration", async (params) => {
return services.issues.getOrchestrationSummary(params);
}),
"issues.listComments": gated("issues.listComments", async (params) => {
return services.issues.listComments(params);
}),
"issues.createComment": gated("issues.createComment", async (params) => {
if (params.actorUserId && !capabilitySet.has("issue.comments.create_human_attributed")) {
throw new CapabilityDeniedError(
pluginId,
"issues.createComment",
"issue.comments.create_human_attributed",
);
}
return services.issues.createComment(params);
}),
"issues.createInteraction": gated("issues.createInteraction", async (params) => {
return services.issues.createInteraction(params);
}),
"issues.listInteractions": gated("issues.listInteractions", async (params) => {
return services.issues.listInteractions(params);
}),
"issues.respondInteraction": gated("issues.respondInteraction", async (params) => {
return services.issues.respondInteraction(params);
}),
"issues.listAttachments": gated("issues.listAttachments", async (params) => {
return services.issues.listAttachments(params);
}),
"issues.getAttachmentContent": gated("issues.getAttachmentContent", async (params) => {
return services.issues.getAttachmentContent(params);
}),
// Approvals
"approvals.list": gated("approvals.list", async (params) => {
return services.approvals.list(params);
}),
"approvals.get": gated("approvals.get", async (params) => {
return services.approvals.get(params);
}),
"approvals.decide": gated("approvals.decide", async (params) => {
return services.approvals.decide(params);
}),
// Issue Documents
"issues.documents.list": gated("issues.documents.list", async (params) => {
return services.issueDocuments.list(params);
}),
"issues.documents.get": gated("issues.documents.get", async (params) => {
return services.issueDocuments.get(params);
}),
"issues.documents.upsert": gated("issues.documents.upsert", async (params) => {
return services.issueDocuments.upsert(params);
}),
"issues.documents.delete": gated("issues.documents.delete", async (params) => {
return services.issueDocuments.delete(params);
}),
// Agents
"agents.list": gated("agents.list", async (params) => {
return services.agents.list(params);
}),
"agents.getRuntimeIdentity": gated("agents.getRuntimeIdentity", async (params) => {
if (!services.agents.getRuntimeIdentity) throw new Error("Agent runtime identity is unavailable");
return services.agents.getRuntimeIdentity(params);
}),
"agents.getRuntime": gated("agents.getRuntime", async (params) => {
if (!services.agents.getRuntime) throw new Error("Agent runtime state is unavailable");
return services.agents.getRuntime(params);
}),
"agents.get": gated("agents.get", async (params) => {
return services.agents.get(params);
}),
"agents.pause": gated("agents.pause", async (params) => {
return services.agents.pause(params);
}),
"agents.resume": gated("agents.resume", async (params) => {
return services.agents.resume(params);
}),
"agents.invoke": gated("agents.invoke", async (params) => {
return services.agents.invoke(params);
}),
"agents.managed.get": gated("agents.managed.get", async (params) => {
return services.agents.managedGet(params);
}),
"agents.managed.reconcile": gated("agents.managed.reconcile", async (params) => {
return services.agents.managedReconcile(params);
}),
"agents.managed.reset": gated("agents.managed.reset", async (params) => {
return services.agents.managedReset(params);
}),
// Agent Sessions
"agents.sessions.create": gated("agents.sessions.create", async (params) => {
return services.agentSessions.create(params);
}),
"agents.sessions.list": gated("agents.sessions.list", async (params) => {
return services.agentSessions.list(params);
}),
"agents.sessions.sendMessage": gated("agents.sessions.sendMessage", async (params) => {
return services.agentSessions.sendMessage(params);
}),
"agents.sessions.close": gated("agents.sessions.close", async (params) => {
return services.agentSessions.close(params);
}),
// Goals
"goals.list": gated("goals.list", async (params) => {
return services.goals.list(params);
}),
"goals.get": gated("goals.get", async (params) => {
return services.goals.get(params);
}),
"goals.create": gated("goals.create", async (params) => {
return services.goals.create(params);
}),
"goals.update": gated("goals.update", async (params) => {
return services.goals.update(params);
}),
// Access
"access.members.list": gated("access.members.list", async (params) => {
return services.access.listMembers(params);
}),
"access.members.get": gated("access.members.get", async (params) => {
return services.access.getMember(params);
}),
"access.members.update": gated("access.members.update", async (params) => {
return services.access.updateMember(params);
}),
"access.invites.list": gated("access.invites.list", async (params) => {
return services.access.listInvites(params);
}),
"access.invites.create": gated("access.invites.create", async (params) => {
return services.access.createInvite(params);
}),
"access.invites.revoke": gated("access.invites.revoke", async (params) => {
return services.access.revokeInvite(params);
}),
// Authorization
"authorization.grants.list": gated("authorization.grants.list", async (params) => {
return services.authorization.listGrants(params);
}),
"authorization.grants.set": gated("authorization.grants.set", async (params) => {
return services.authorization.setGrants(params);
}),
"authorization.policies.summary": gated("authorization.policies.summary", async (params) => {
return services.authorization.policySummary(params);
}),
"authorization.policies.get": gated("authorization.policies.get", async (params) => {
return services.authorization.getPolicy(params);
}),
"authorization.policies.update": gated("authorization.policies.update", async (params) => {
return services.authorization.updatePolicy(params);
}),
"authorization.policies.previewAssignment": gated("authorization.policies.previewAssignment", async (params) => {
return services.authorization.previewAssignment(params);
}),
"authorization.policies.explainAssignment": gated("authorization.policies.explainAssignment", async (params) => {
return services.authorization.explainAssignment(params);
}),
"authorization.audit.search": gated("authorization.audit.search", async (params) => {
return services.authorization.searchAudit(params);
}),
};
}
// ---------------------------------------------------------------------------
// Utility: getRequiredCapability
// ---------------------------------------------------------------------------
/**
* Get the capability required for a given worker→host method, or `null` if
* no capability is required.
*
* Useful for inspecting capability requirements without calling the factory.
*
* @param method - The worker→host method name
* @returns The required capability, or `null`
*/
export function getRequiredCapability(
method: WorkerToHostMethodName,
): PluginCapability | null {
return METHOD_CAPABILITY_MAP[method];
}