@polycore/runner 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/action.ts +3 -3
- package/src/cli.ts +1 -1
- package/src/connect.ts +4 -1
- package/src/connector.ts +24 -12
- package/src/connectors/firestore.ts +4 -4
- package/src/index.ts +3 -3
- package/src/workload/capability.ts +15 -12
- package/src/workload/container-backend.ts +5 -3
- package/src/workload/types.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycore/runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Polycore runner: executes governed, read-only-constrained connectors against a customer's datastores. Runs in the customer's own infrastructure; holds scoped credentials and connects outbound to the control plane.",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"tsx": "4.21.0",
|
|
31
31
|
"ws": "8.18.3",
|
|
32
32
|
"zod": "4.4.3",
|
|
33
|
-
"@polycore/protocol": "^0.
|
|
33
|
+
"@polycore/protocol": "^0.5.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "25.6.2",
|
package/src/action.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { z } from "zod";
|
|
|
14
14
|
* its environment — the credential never leaves the runner's host.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
export type
|
|
17
|
+
export type ActionHazard = "read" | "write";
|
|
18
18
|
|
|
19
19
|
/** Human-facing progress sink, mirroring the desktop SDK's `ctx.log`. */
|
|
20
20
|
export interface ActionLogger {
|
|
@@ -39,11 +39,11 @@ export interface Action<Input = unknown, Output = unknown> {
|
|
|
39
39
|
readonly description: string;
|
|
40
40
|
readonly tags?: readonly string[];
|
|
41
41
|
/**
|
|
42
|
-
* Hazard class. Defaults to `
|
|
42
|
+
* Hazard class. Defaults to `write` — the safe default, since an
|
|
43
43
|
* unclassified operation must be treated as dangerous and routed through
|
|
44
44
|
* the approval gate. Declare `read` for idempotent, side-effect-free work.
|
|
45
45
|
*/
|
|
46
|
-
readonly
|
|
46
|
+
readonly hazard?: ActionHazard;
|
|
47
47
|
readonly input: z.ZodType<Input>;
|
|
48
48
|
readonly output: z.ZodType<Output>;
|
|
49
49
|
/** Secret keys this action needs, mapped to a human description. */
|
package/src/cli.ts
CHANGED
|
@@ -156,7 +156,7 @@ async function main(): Promise<void> {
|
|
|
156
156
|
for (const [slug, registry] of registries) {
|
|
157
157
|
for (const capability of registry.list()) {
|
|
158
158
|
process.stdout.write(
|
|
159
|
-
`[${slug}] ${capability.name} [${capability.kind}/${capability.
|
|
159
|
+
`[${slug}] ${capability.name} [${capability.kind}/${capability.hazard}] ${capability.description}\n`,
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
}
|
package/src/connect.ts
CHANGED
|
@@ -23,9 +23,12 @@ export function describeConnectors(
|
|
|
23
23
|
return registry.list().map((capability) => ({
|
|
24
24
|
name: capability.name,
|
|
25
25
|
description: capability.description,
|
|
26
|
-
|
|
26
|
+
hazard: capability.hazard,
|
|
27
27
|
kind: capability.kind,
|
|
28
28
|
params: z.toJSONSchema(capability.params),
|
|
29
|
+
...(capability.output === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: { result: z.toJSONSchema(capability.output) }),
|
|
29
32
|
}));
|
|
30
33
|
}
|
|
31
34
|
|
package/src/connector.ts
CHANGED
|
@@ -4,14 +4,23 @@ import type { Action, ActionLogger, LoadedAction } from "./action.js";
|
|
|
4
4
|
import type { EnvironmentConfig, ProjectConfig } from "./config.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Hazard class for a connector. Lane 1 connectors are always `read`:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Hazard class for a connector. Lane 1 connectors are always `read`: they exist
|
|
8
|
+
* precisely because they are mutation-incapable, which is what lets them run
|
|
9
|
+
* with no human approval. Mutating operations are modeled as blessed *actions*,
|
|
10
|
+
* not connectors, and run behind the approval gate.
|
|
10
11
|
*/
|
|
11
|
-
export type
|
|
12
|
+
export type ConnectorHazard = "read";
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Hazard class of any capability, the honest description of what it can do and
|
|
16
|
+
* therefore how it must be gated:
|
|
17
|
+
* - `read`: no side effects, runs with no approval.
|
|
18
|
+
* - `write`: a known, human-declared mutation, runs behind one human approval.
|
|
19
|
+
* - `unknown`: arbitrary code whose effect cannot be established from a trusted
|
|
20
|
+
* declaration (a delegated workload). A human must review the code before it
|
|
21
|
+
* runs; it is never auto-run and is never labeled a "mutation".
|
|
22
|
+
*/
|
|
23
|
+
export type Hazard = "read" | "write" | "unknown";
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
26
|
* Whether a capability is a built-in connector, a customer-authored action,
|
|
@@ -42,8 +51,8 @@ export interface Connector<Args = unknown, Result = unknown> {
|
|
|
42
51
|
readonly name: string;
|
|
43
52
|
/** One-line description for catalogs and the agent. */
|
|
44
53
|
readonly description: string;
|
|
45
|
-
/** Always `read`: see {@link
|
|
46
|
-
readonly
|
|
54
|
+
/** Always `read`: see {@link ConnectorHazard}. */
|
|
55
|
+
readonly hazard: ConnectorHazard;
|
|
47
56
|
/**
|
|
48
57
|
* Which environment-config family this connector needs (e.g. `firestore`).
|
|
49
58
|
* A built-in connector is registered for a project only when at least one
|
|
@@ -81,9 +90,11 @@ export function connectorAvailableFor(
|
|
|
81
90
|
export interface RegisteredCapability {
|
|
82
91
|
readonly name: string;
|
|
83
92
|
readonly description: string;
|
|
84
|
-
readonly
|
|
93
|
+
readonly hazard: Hazard;
|
|
85
94
|
readonly kind: CapabilityKind;
|
|
86
95
|
readonly params: z.ZodType;
|
|
96
|
+
/** The result schema, when the capability declares one (actions do). */
|
|
97
|
+
readonly output?: z.ZodType;
|
|
87
98
|
run(args: unknown, ctx: ConnectorContext): Promise<unknown>;
|
|
88
99
|
}
|
|
89
100
|
|
|
@@ -158,7 +169,7 @@ export class ConnectorRegistry {
|
|
|
158
169
|
this.add({
|
|
159
170
|
name: connector.name,
|
|
160
171
|
description: connector.description,
|
|
161
|
-
|
|
172
|
+
hazard: connector.hazard,
|
|
162
173
|
kind: "connector",
|
|
163
174
|
params: connector.params,
|
|
164
175
|
run: (args, ctx) => connector.run(args, ctx),
|
|
@@ -174,7 +185,7 @@ export class ConnectorRegistry {
|
|
|
174
185
|
/**
|
|
175
186
|
* Register a pre-built capability directly. Used for capabilities that are
|
|
176
187
|
* neither a read-only `Connector` nor a folder-loaded action, e.g. the
|
|
177
|
-
* `workload.execute` executor, whose `
|
|
188
|
+
* `workload.execute` executor, whose `hazard` is `unknown` and whose `run`
|
|
178
189
|
* bridges into a sandbox backend.
|
|
179
190
|
*/
|
|
180
191
|
public registerCapability(capability: RegisteredCapability): void {
|
|
@@ -191,9 +202,10 @@ export class ConnectorRegistry {
|
|
|
191
202
|
this.add({
|
|
192
203
|
name: id,
|
|
193
204
|
description: action.description,
|
|
194
|
-
|
|
205
|
+
hazard: action.hazard ?? "write",
|
|
195
206
|
kind: "action",
|
|
196
207
|
params: action.input,
|
|
208
|
+
output: action.output,
|
|
197
209
|
run: (args, ctx) =>
|
|
198
210
|
Promise.resolve(
|
|
199
211
|
action.run({
|
|
@@ -100,7 +100,7 @@ const firestoreQuery: Connector<z.infer<typeof queryParams>, unknown> = {
|
|
|
100
100
|
name: "firestore.query",
|
|
101
101
|
description:
|
|
102
102
|
"Read documents from a Firestore collection with optional where filters, ordering, and a limit. Returns at most `limit` documents (a capped page for inspection); do NOT use it to count or total; use firestore.count for that. Read-only.",
|
|
103
|
-
|
|
103
|
+
hazard: "read",
|
|
104
104
|
configKey: "firestore",
|
|
105
105
|
params: queryParams,
|
|
106
106
|
async run(args, ctx) {
|
|
@@ -133,7 +133,7 @@ const firestoreGet: Connector<z.infer<typeof getParams>, unknown> = {
|
|
|
133
133
|
name: "firestore.get",
|
|
134
134
|
description:
|
|
135
135
|
"Read a single Firestore document by collection and id. Read-only.",
|
|
136
|
-
|
|
136
|
+
hazard: "read",
|
|
137
137
|
configKey: "firestore",
|
|
138
138
|
params: getParams,
|
|
139
139
|
async run(args, ctx) {
|
|
@@ -156,7 +156,7 @@ const firestoreCount: Connector<z.infer<typeof countParams>, number> = {
|
|
|
156
156
|
name: "firestore.count",
|
|
157
157
|
description:
|
|
158
158
|
"Count documents in a Firestore collection (optionally filtered) using a server-side aggregation. It counts EVERY matching document, so it is the correct, exact tool for any 'how many' / total question. For 'how many <record>s', count the <record> collection (e.g. users → the \"users\" collection) with NO filter unless a subset is explicitly asked for; if unsure of the exact collection name, call firestore.listCollections first. Read-only.",
|
|
159
|
-
|
|
159
|
+
hazard: "read",
|
|
160
160
|
configKey: "firestore",
|
|
161
161
|
params: countParams,
|
|
162
162
|
async run(args, ctx) {
|
|
@@ -172,7 +172,7 @@ const listCollections: Connector<Record<string, never>, string[]> = {
|
|
|
172
172
|
name: "firestore.listCollections",
|
|
173
173
|
description:
|
|
174
174
|
"List the top-level Firestore collection ids. Call this FIRST to ground yourself whenever you are not certain which collection holds the kind of record the question is about, then count/query that collection. Map the record asked about (a 'user', an 'order', a 'workspace') to its collection. The name of an organization, product, team, or environment is NEVER a collection: it only identifies whose system you are querying, so never treat it as a collection, id, or filter. Read-only.",
|
|
175
|
-
|
|
175
|
+
hazard: "read",
|
|
176
176
|
configKey: "firestore",
|
|
177
177
|
params: z.object({}),
|
|
178
178
|
async run(_args, ctx) {
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type {
|
|
|
22
22
|
|
|
23
23
|
export type {
|
|
24
24
|
Action,
|
|
25
|
-
|
|
25
|
+
ActionHazard,
|
|
26
26
|
ActionLogger,
|
|
27
27
|
ActionRunContext,
|
|
28
28
|
LoadedAction,
|
|
@@ -62,12 +62,12 @@ export {
|
|
|
62
62
|
RunnerClient,
|
|
63
63
|
} from "./connect.js";
|
|
64
64
|
export type {
|
|
65
|
-
CapabilityEffect,
|
|
66
65
|
CapabilityKind,
|
|
67
66
|
Connector,
|
|
68
67
|
ConnectorContext,
|
|
69
|
-
|
|
68
|
+
ConnectorHazard,
|
|
70
69
|
DispatchResult,
|
|
70
|
+
Hazard,
|
|
71
71
|
RegisteredCapability,
|
|
72
72
|
SecretSource,
|
|
73
73
|
} from "./connector.js";
|
|
@@ -21,13 +21,15 @@ const WORKLOAD_DESCRIPTION =
|
|
|
21
21
|
"Run a short program you author, in an isolated sandbox on the runner host, " +
|
|
22
22
|
"when no other connector or action covers the task. Use this for arbitrary " +
|
|
23
23
|
"computation and multi-step work: parsing files, reconciling systems, batch " +
|
|
24
|
-
"operations, calling an API no connector exposes. Provide `
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"to
|
|
29
|
-
"
|
|
30
|
-
"
|
|
24
|
+
"operations, calling an API no connector exposes. Provide `title` (a short " +
|
|
25
|
+
'human-readable label for what the program does, e.g. "Credit top 10 active ' +
|
|
26
|
+
'users $5", shown to the human reviewer), `files` (source, at least the ' +
|
|
27
|
+
"entrypoint; include a package.json to use npm libraries), `entrypoint` (the " +
|
|
28
|
+
"file to run; .ts runs under tsx), `secrets` (keys of the credentials the " +
|
|
29
|
+
"code needs, injected as env vars), and set `egress: true` to allow network " +
|
|
30
|
+
"access or dependency installs. It ALWAYS pauses for a human to review the " +
|
|
31
|
+
"exact code before running, so propose the workload directly rather than " +
|
|
32
|
+
"asking the user for permission first. Read its stdout for the result.";
|
|
31
33
|
|
|
32
34
|
/** Build the container backend the runner uses to execute workloads. */
|
|
33
35
|
export function createSandboxBackend(config: SandboxConfig): SandboxBackend {
|
|
@@ -72,10 +74,11 @@ export interface WorkloadCapabilityOptions {
|
|
|
72
74
|
/**
|
|
73
75
|
* The `workload.execute` capability: validate the spec, resolve exactly the
|
|
74
76
|
* requested credentials, clamp the limits, and run it in the sandbox backend.
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
77
|
+
* Its hazard is `unknown` (not `write`): its effect can't be established from a
|
|
78
|
+
* trusted declaration, only from the code itself, so the control plane always
|
|
79
|
+
* routes it through human *code* review, the trust transition that turns
|
|
80
|
+
* agent-authored code into trusted-for-one-run code. The full spec (including
|
|
81
|
+
* the source) is the approved artifact; its digest is returned for audit.
|
|
79
82
|
*/
|
|
80
83
|
export function createWorkloadCapability(
|
|
81
84
|
backend: SandboxBackend,
|
|
@@ -85,7 +88,7 @@ export function createWorkloadCapability(
|
|
|
85
88
|
return {
|
|
86
89
|
name: WORKLOAD_CAPABILITY_NAME,
|
|
87
90
|
description: WORKLOAD_DESCRIPTION,
|
|
88
|
-
|
|
91
|
+
hazard: "unknown",
|
|
89
92
|
kind: "workload",
|
|
90
93
|
params: workloadSpecSchema,
|
|
91
94
|
run: async (rawArgs: unknown, ctx: ConnectorContext) => {
|
|
@@ -53,14 +53,16 @@ function shellSingleQuote(value: string): string {
|
|
|
53
53
|
/**
|
|
54
54
|
* The in-container command. `.ts`/`.tsx` entrypoints run under `tsx`, others
|
|
55
55
|
* under `node`. When `install` is set, dependencies are installed first (this
|
|
56
|
-
* requires network egress)
|
|
57
|
-
*
|
|
56
|
+
* requires network egress); its noisy output is redirected to stderr so it
|
|
57
|
+
* never contaminates the workload's stdout, which is the channel the result is
|
|
58
|
+
* read from. `exec` replaces the shell so signals reach the process directly
|
|
59
|
+
* for prompt timeout kills.
|
|
58
60
|
*/
|
|
59
61
|
export function buildRunScript(entrypoint: string, install: boolean): string {
|
|
60
62
|
const runner = /\.tsx?$/.test(entrypoint) ? "tsx" : "node";
|
|
61
63
|
const run = `exec ${runner} ${shellSingleQuote(entrypoint)}`;
|
|
62
64
|
if (!install) return run;
|
|
63
|
-
return `npm install --omit=dev --no-audit --no-fund && ${run}`;
|
|
65
|
+
return `npm install --omit=dev --no-audit --no-fund >&2 && ${run}`;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export interface ContainerArgsParams {
|
package/src/workload/types.ts
CHANGED
|
@@ -35,6 +35,13 @@ export type WorkloadLimits = z.infer<typeof workloadLimitsSchema>;
|
|
|
35
35
|
* may use, whether it may reach the network, and its resource envelope.
|
|
36
36
|
*/
|
|
37
37
|
export const workloadSpecSchema = z.object({
|
|
38
|
+
/**
|
|
39
|
+
* A short, human-readable label for what the program does (e.g. "Credit top
|
|
40
|
+
* 10 active users $5"). Shown to the human reviewer on the approval card so
|
|
41
|
+
* they see the intent before reading the code. Purely descriptive: it is not
|
|
42
|
+
* part of the artifact digest, since it does not affect what executes.
|
|
43
|
+
*/
|
|
44
|
+
title: z.string().optional(),
|
|
38
45
|
/** Source files of the program (at least the entrypoint). */
|
|
39
46
|
files: z.array(workloadFileSchema).min(1),
|
|
40
47
|
/** Which file to run; must be one of `files`, a safe relative path. */
|