@cr1ms0n/pi-subagent 0.9.0 → 0.10.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/docs/SECURITY.md CHANGED
@@ -6,11 +6,11 @@ and can use tools according to their capability profile.
6
6
 
7
7
  ## What subagents can do
8
8
 
9
- | Profile | Default tools | Writes? |
10
- |---------|---------------|---------|
11
- | `explore` | `read`, `grep`, `find`, `ls` (+ safe extras and Pi context tools) | No project-file writes |
12
- | `review` | same as explore | No project-file writes |
13
- | `general` | Jev-chosen subset of the full available locally permitted catalog (+ Pi context tools) | Yes if the selected tools include `bash`/`edit`/`write` |
9
+ | Profile | Finalized tools | Writes? |
10
+ |---------|-----------------|---------|
11
+ | `explore` | Jev-chosen subset of locally permitted read-only candidates, plus available Pi context tools | No project-file writes |
12
+ | `review` | Same as explore | No project-file writes |
13
+ | `general` | Jev-chosen subset of the full available locally permitted catalog, plus available Pi context tools | Yes if write-capable tools are selected |
14
14
 
15
15
  Parallel mode defaults to `explore` to avoid concurrent shared writes.
16
16
 
@@ -50,20 +50,20 @@ Parallel mode defaults to `explore` to avoid concurrent shared writes.
50
50
  (RPC mode). Extension UI dialogs raised inside a child are auto-cancelled so
51
51
  they can never hang a run — which also means a child can never obtain
52
52
  interactive consent. Prefer restricted tools for async/background runs.
53
- 11. **Steering messages** (`action: "steer"` and the overlay `s` key) inject text
54
- into a running child's conversation with user-level authority. Anything that
55
- can call the subagent tool can steer any live run in the same session.
56
- 7. **Process cleanup.** On POSIX, children run in their own process group so tree
53
+ 7. **Steering messages** (`action: "steer"` and the overlay `s` key) inject text
54
+ into a running child's conversation with user-level authority. Anything that
55
+ can call the subagent tool can steer any live run in the same session.
56
+ 8. **Process cleanup.** On POSIX, children run in their own process group so tree
57
57
  kills work for ordinary descendants. Parent (re)start reaps orphans recorded
58
58
  under `~/.pi/subagent-locks/runs/` so resume cannot race a still-alive writer.
59
59
  Grandchildren that call `setsid()` can still escape a simple process-group kill.
60
- 8. **Resume exclusivity.** Direct resume takes a durable per-session file lock;
60
+ 9. **Resume exclusivity.** Direct resume takes a durable per-session file lock;
61
61
  concurrent parents cannot append to the same child session.
62
- 9. **Profiles are tool-selection policy, not a sandbox.** Children inherit
62
+ 10. **Profiles are tool-selection policy, not a sandbox.** Children inherit
63
63
  `$HOME`, SSH/cloud credentials, network access, and the parent filesystem.
64
64
  Git worktrees only isolate the checkout. For untrusted tasks, use an outer
65
65
  container/cgroup/network policy.
66
- 10. **`max_cost` is accounting, not a hard provider gate.** Usage arrives after a
66
+ 11. **`max_cost` is accounting, not a hard provider gate.** Usage arrives after a
67
67
  turn; orphans may spend money the ledger never sees. It caps provider-reported
68
68
  execution cost only: TypeSafe reports routing tokens, not currency, so selector
69
69
  cost is unreported and outside `max_cost`. Combine with provider account
@@ -81,12 +81,11 @@ and synthesis select from the new task instruction rather than the assembled
81
81
  transcript. Task text and model descriptions are user content and can themselves
82
82
  contain secrets; there is no guaranteed redaction.
83
83
 
84
- The TypeSafe credential is read from the environment variable named by
85
- `jevRouting.apiKeyEnv` (default `TYPESAFE_API_KEY`) and sent only as an
86
- `Authorization` header to the fixed official HTTPS endpoint, with redirects
87
- disabled. It is never stored in the config file, prompts, argv, logs or results.
88
- Rotate any credential that has been pasted into a transcript or shared in
89
- conversation.
84
+ Version `0.10.0` reads the TypeSafe credential from `jevRouting.apiKey` in the private user-level `~/.pi/subagent.json`. This is plaintext storage: restrict file access and protect editor backups and synchronized copies. Same-user processes, including children with filesystem access, may read it. Profiles and worktrees do not protect this file from those processes.
85
+
86
+ The transport sends the key only as an `Authorization` header to the fixed official HTTPS endpoint, with redirects disabled. It does not automatically copy the key into prompts, selector JSON bodies, argv, child manifests, logs, receipts or results. Never serialize or log the complete routing configuration. Rotate any credential pasted into a transcript or shared in conversation.
87
+
88
+ Published npm `0.9.0` uses the older environment-based mechanism. In `0.10.0`, `apiKeyEnv` is rejected with manual migration guidance and no environment fallback. Unrelated `PI_SUBAGENT_*` runtime settings remain supported.
90
89
 
91
90
  New extension-managed dispatch is Pi-only. A `backend: "codex"` or
92
91
  `backend: "claude"` new task is rejected before any selector or provider work,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cr1ms0n/pi-subagent",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Community fork of Luke Parke's pi-subagent with Jev model/tool routing and verified Pi child capabilities",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,6 +10,13 @@
10
10
  "cr1ms0n (fork maintainer)"
11
11
  ],
12
12
  "homepage": "https://www.npmjs.com/package/@cr1ms0n/pi-subagent",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/awoaCrim/pi-smart-subagents.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/awoaCrim/pi-smart-subagents/issues"
19
+ },
13
20
  "keywords": [
14
21
  "pi-package",
15
22
  "pi",
@@ -31,6 +38,7 @@
31
38
  "skills",
32
39
  "docs",
33
40
  "README.md",
41
+ "README.zh-CN.md",
34
42
  "CHANGELOG.md",
35
43
  "LICENSE"
36
44
  ],
@@ -136,7 +136,11 @@ An optional candidate `thinking` value is an opaque Pi thinking-level string;
136
136
  common values include `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and
137
137
  `max`, but model-specific values are passed through unchanged. It is a default:
138
138
  explicit task, agent, and profile `taskDefaults.thinking` values override it.
139
- The extension re-reads `jevRouting` on each dispatch and injects the current
140
- routing guidance into the parent prompt. If `jevRouting` is missing or invalid,
141
- or the credential environment variable is unset, management remains available but
142
- new spawns, `/btw`, plan, resume, fork and synthesis are rejected.
139
+ The extension re-reads `jevRouting` on each dispatch and injects non-secret
140
+ routing guidance into the parent prompt. The user stores the TypeSafe credential
141
+ in `jevRouting.apiKey` in the private `~/.pi/subagent.json`; do not read, display
142
+ or copy the key into task text, prompts or output. Legacy `apiKeyEnv` is rejected
143
+ with migration guidance; there is no environment fallback. If the config or key
144
+ is missing or invalid, management remains available but new spawns, `/btw`, plan,
145
+ resume, fork and synthesis are rejected. This config-file credential contract ships in
146
+ npm 0.10.0; published npm 0.9.0 uses the old environment mechanism.
package/src/config.ts CHANGED
@@ -61,7 +61,7 @@ export interface SubagentConfig {
61
61
  lockRetentionDays: number;
62
62
  /** Legacy per-profile defaults. Model/fallback fields are retained for config compatibility but ignored by Jev routing. */
63
63
  taskDefaults?: TaskDefaultsByProfile;
64
- /** Mandatory, user-owned Jev candidate configuration. Never contains a credential. */
64
+ /** Private Jev configuration including apiKey. Never log or serialize the whole config. */
65
65
  jevRouting?: JevRoutingConfig;
66
66
  /** Safe migration or parse failure; existing-run management remains available. */
67
67
  jevRoutingError?: string;
package/src/jev-router.ts CHANGED
@@ -2,6 +2,7 @@ import { Buffer } from "node:buffer";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { Semaphore } from "./semaphore.js";
4
4
  import { isThinkingLevel } from "./thinking.js";
5
+ import { normalizeRoutingApiKey } from "./routing-policy.js";
5
6
  import {
6
7
  DEFAULT_ROUTING_CONCURRENCY,
7
8
  MAX_ROUTING_MODEL_ID_LENGTH,
@@ -58,7 +59,7 @@ export type {
58
59
  * - A single logical deadline = min(config.timeoutMs, caller absolute deadline) spans every
59
60
  * request and all limiter waiting. Concurrent HTTP requests are bounded to two by default.
60
61
  * - Only `https://api.typesafe.ai/v1/systemone` with `redirect:"error"`; the Bearer key comes
61
- * from the injected environment accessor and never appears in config, results or messages.
62
+ * from the private config snapshot and is never copied into request bodies, results or messages.
62
63
  * - Responses are untrusted data: shape, answer type, question set, allowed options, finite
63
64
  * probabilities, probability sum tolerance, confidence, usage counts and selector version
64
65
  * are validated. Valid low-confidence choices are accepted (no threshold, no substitution).
@@ -72,7 +73,7 @@ export type {
72
73
  * - Failures return a discriminated result that includes every available receipt; there is no
73
74
  * automatic selector retry, fallback or emergency model.
74
75
  *
75
- * Collaborators (`fetchImpl`, `env`, `now`, `idFactory`, `limiter`, `onReceipt`) are all
76
+ * Collaborators (`fetchImpl`, `now`, `idFactory`, `limiter`, `onReceipt`) are all
76
77
  * injectable so the whole surface is testable offline with zero provider calls.
77
78
  */
78
79
 
@@ -82,12 +83,10 @@ export interface RoutingLimiter {
82
83
  }
83
84
 
84
85
  export interface JevRouterOptions {
85
- /** Frozen per-invocation config snapshot. */
86
+ /** Frozen per-invocation config snapshot containing a private credential. Never log it. */
86
87
  config: JevRoutingConfig;
87
88
  /** Injected transport; defaults to global `fetch`. */
88
89
  fetchImpl?: typeof fetch;
89
- /** Injected environment accessor; defaults to `process.env`. */
90
- env?: (name: string) => string | undefined;
91
90
  /** Injected clock; defaults to `Date.now`. */
92
91
  now?: () => number;
93
92
  /** Injected unique-ID factory; defaults to `randomUUID`. */
@@ -411,7 +410,7 @@ function validateAnswers(body: unknown, questions: readonly QuestionSpec[]): Ans
411
410
 
412
411
  function selectorStatusFailure(status: number): { code: RoutingFailureCode; message: string } {
413
412
  if (status === 401 || status === 403) {
414
- return { code: "unauthorized", message: "TypeSafe rejected the routing credential (HTTP 401/403). Check the configured apiKeyEnv environment variable." };
413
+ return { code: "unauthorized", message: "TypeSafe rejected the routing credential (HTTP 401/403). Check jevRouting.apiKey in your private ~/.pi/subagent.json configuration." };
415
414
  }
416
415
  if (status === 422) {
417
416
  return { code: "invalid_request", message: "TypeSafe rejected the routing request as invalid (HTTP 422). Check selectorModel and the configured candidate/tool descriptions." };
@@ -607,7 +606,6 @@ function freezeReceipt(draft: ReceiptDraft): RoutingReceipt {
607
606
  export class JevRouter {
608
607
  private readonly config: JevRoutingConfig;
609
608
  private readonly fetchImpl: typeof fetch | undefined;
610
- private readonly env: (name: string) => string | undefined;
611
609
  private readonly now: () => number;
612
610
  private readonly idFactory: () => string;
613
611
  private readonly limiter: RoutingLimiter;
@@ -616,7 +614,6 @@ export class JevRouter {
616
614
  constructor(options: JevRouterOptions) {
617
615
  this.config = options.config;
618
616
  this.fetchImpl = options.fetchImpl ?? (typeof globalThis.fetch === "function" ? globalThis.fetch : undefined);
619
- this.env = options.env ?? ((name) => process.env[name]);
620
617
  this.now = options.now ?? Date.now;
621
618
  this.idFactory = options.idFactory ?? randomUUID;
622
619
  this.limiter = options.limiter ?? sharedLimiter;
@@ -647,12 +644,11 @@ export class JevRouter {
647
644
  return this.fail("too_many_tools", `At most ${MAX_ROUTING_TOOL_QUESTIONS} eligible tools can be considered in one selection.`, call);
648
645
  }
649
646
 
650
- const rawKey = this.env(this.config.apiKeyEnv);
651
- const apiKey = typeof rawKey === "string" ? rawKey.trim() : "";
647
+ const apiKey = normalizeRoutingApiKey(this.config.apiKey);
652
648
  if (!apiKey) {
653
649
  return this.fail(
654
650
  "missing_api_key",
655
- `The TypeSafe routing credential is missing: set the ${this.config.apiKeyEnv} environment variable locally, then retry the dispatch.`,
651
+ "The TypeSafe routing credential is missing or invalid: set jevRouting.apiKey in your private ~/.pi/subagent.json to a non-blank key without embedded whitespace or control characters, then retry the dispatch.",
656
652
  call,
657
653
  );
658
654
  }
@@ -1,6 +1,5 @@
1
1
  import { isThinkingLevel } from "./thinking.js";
2
2
  import {
3
- DEFAULT_API_KEY_ENV,
4
3
  DEFAULT_ROUTING_TIMEOUT_MS,
5
4
  DEFAULT_SELECTOR_MODEL,
6
5
  MAX_ROUTING_MODEL_ID_LENGTH,
@@ -30,22 +29,21 @@ export type {
30
29
  * - `parseJevRouting(raw, source?)` parses the `jevRouting` **subtree** (not the whole
31
30
  * `~/.pi/subagent.json` file) and returns an immutable snapshot. It throws a plain
32
31
  * `Error` with an actionable message on any unknown field, duplicate/blank model ID,
33
- * blank description, invalid environment-variable name, non-integer/out-of-range timeout
34
- * or unsupported candidate count. Callers (`src/config.ts`) catch that message and expose
35
- * it as a config error; no credential or provider catalog is read.
32
+ * blank description, missing/invalid credential, non-integer/out-of-range timeout
33
+ * or unsupported candidate count. Callers (`src/config.ts`) expose safe error messages;
34
+ * the parser never reads the environment or a provider catalog.
36
35
  * - `formatJevRoutingPrompt` renders model-facing guidance and works with **no** config, no
37
36
  * credential and no inference, so management actions stay independent of routing setup.
38
37
  * - The candidate helpers are pure; they never contact Pi or TypeSafe.
39
38
  *
40
- * The credential *value* must never be stored in config: only the environment-variable name
41
- * is parsed and returned. The parser rejects unknown fields, so an accidental `apiKey` or
42
- * `apiKeyValue` field is an error rather than silently ignored.
39
+ * The private snapshot contains the credential from `apiKey`. Never serialize it into
40
+ * model-facing guidance or selector input. Credential diagnostics contain no supplied
41
+ * values. Legacy `apiKeyEnv` is rejected with manual migration guidance, without lookup.
43
42
  */
44
43
 
45
44
  /** Default config-file label used in prose (owned by `config.ts` for file reads). */
46
45
  export const JEV_ROUTING_CONFIG_FILE = "~/.pi/subagent.json";
47
46
 
48
- const ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
49
47
  /** Exact provider/model ID: at least one slash, further ID slashes allowed; no whitespace, control chars or globs. */
50
48
  const MODEL_ID = /^[^\s\u0000-\u001f\u007f/*?]+(?:\/[^\s\u0000-\u001f\u007f/*?]+)+$/u;
51
49
  const MAX_GUIDANCE_MODEL_LINES = 50;
@@ -76,12 +74,19 @@ function parseSelectorModel(value: unknown, source: string): string {
76
74
  return model;
77
75
  }
78
76
 
79
- function parseApiKeyEnv(value: unknown, source: string): string {
80
- if (value === undefined) return DEFAULT_API_KEY_ENV;
81
- if (typeof value !== "string" || !ENV_VAR_NAME.test(value.trim())) {
82
- invalid(source, "apiKeyEnv must be an environment-variable name such as TYPESAFE_API_KEY (the credential value must not be stored in config)");
77
+ /** Shared credential normalization for config parsing and defensive transport validation. */
78
+ export function normalizeRoutingApiKey(value: unknown): string | undefined {
79
+ if (typeof value !== "string") return undefined;
80
+ const key = value.trim();
81
+ return key && !/[\s\u0000-\u001f\u007f-\u009f]/u.test(key) ? key : undefined;
82
+ }
83
+
84
+ function parseApiKey(value: unknown, source: string): string {
85
+ const key = normalizeRoutingApiKey(value);
86
+ if (key === undefined) {
87
+ invalid(source, "apiKey is required and must be a non-blank key without embedded whitespace or control characters; store it only in your private user config");
83
88
  }
84
- return value.trim();
89
+ return key;
85
90
  }
86
91
 
87
92
  function parseTimeoutMs(value: unknown, source: string): number {
@@ -144,11 +149,14 @@ function parseModels(value: unknown, source: string): readonly JevRoutingModelEn
144
149
  */
145
150
  export function parseJevRouting(raw: unknown, source = JEV_ROUTING_CONFIG_FILE): JevRoutingConfig {
146
151
  const record = requireObject(raw, source, "jevRouting");
147
- rejectUnknownKeys(record, ["selectorModel", "apiKeyEnv", "timeoutMs", "models"], source, "jevRouting");
152
+ if (Object.prototype.hasOwnProperty.call(record, "apiKeyEnv")) {
153
+ invalid(source, "apiKeyEnv is no longer supported; remove it and set jevRouting.apiKey to the credential in your private user config. No environment fallback or automatic migration is performed");
154
+ }
155
+ rejectUnknownKeys(record, ["selectorModel", "apiKey", "timeoutMs", "models"], source, "jevRouting");
148
156
 
149
157
  const snapshot: JevRoutingConfig = {
150
158
  selectorModel: parseSelectorModel(record.selectorModel, source),
151
- apiKeyEnv: parseApiKeyEnv(record.apiKeyEnv, source),
159
+ apiKey: parseApiKey(record.apiKey, source),
152
160
  timeoutMs: parseTimeoutMs(record.timeoutMs, source),
153
161
  models: parseModels(record.models, source),
154
162
  };
@@ -160,7 +168,7 @@ export function jevRoutingTemplate(): string {
160
168
  return JSON.stringify({
161
169
  jevRouting: {
162
170
  selectorModel: DEFAULT_SELECTOR_MODEL,
163
- apiKeyEnv: DEFAULT_API_KEY_ENV,
171
+ apiKey: "<your-typesafe-api-key>",
164
172
  timeoutMs: DEFAULT_ROUTING_TIMEOUT_MS,
165
173
  models: [
166
174
  {
@@ -230,7 +238,7 @@ function routingSummary(config: JevRoutingConfig): string[] {
230
238
  "Every new task/tasks[] spawn, action:\"plan\" request, /btw, resume, fork and synthesis is routed by the Jev selector against the user's candidate-model list.",
231
239
  "Do not pass model or fallback_models: those fields no longer select a route on new work and are rejected. Management actions (status/wait/cancel/steer/diff/apply/discard) never call the selector and need no credential.",
232
240
  `Selector: ${config.selectorModel} (pin an exact version instead of the moving alias to make selection reproducible).`,
233
- `Credential: environment variable ${config.apiKeyEnv}, read locally at request time; the value is never stored in config, prompts, logs or results.`,
241
+ "Credential: jevRouting.apiKey in the private ~/.pi/subagent.json config file. Never read or copy its value into prompts, logs or results; the routing transport uses it only for the Authorization header.",
234
242
  `Logical selection deadline: ${config.timeoutMs} ms, covering all selector requests and waiting for one invocation.`,
235
243
  "Only Pi-backed new dispatch is supported; native Codex/Claude new dispatches are rejected rather than routed.",
236
244
  "Candidate models (exact IDs; the user's per-model characteristics are the matching criteria):",
@@ -259,7 +267,7 @@ export function formatJevRoutingPrompt(config: JevRoutingConfig | undefined, err
259
267
  "## Subagent routing (Jev / TypeSafe)",
260
268
  error || `No valid jevRouting configuration was found in ${JEV_ROUTING_CONFIG_FILE}.`,
261
269
  "Management actions (status/wait/cancel/steer/diff/apply/discard) remain available, but every new task/tasks[] spawn, plan, /btw, resume, fork and synthesis is rejected until jevRouting is configured.",
262
- "Add jevRouting with selectorModel, apiKeyEnv and 1-255 candidate model entries (exact provider/model IDs plus user-written characteristics, including Chinese). The credential itself is read from the named environment variable at request time and is never stored in the config file.",
270
+ "Add jevRouting with selectorModel, apiKey and 1-255 candidate model entries (exact provider/model IDs plus user-written characteristics, including Chinese). The user must store the credential in the private config file, not in chat or source control. Do not read or display the key. Legacy apiKeyEnv is rejected; there is no environment fallback.",
263
271
  "Do not pass model or fallback_models; the selector chooses the execution model and tools.",
264
272
  "Use the package routing template; do not invent model IDs or import legacy modelPolicy entries automatically.",
265
273
  ].join("\n");
@@ -18,8 +18,6 @@ export const TYPESAFE_SYSTEMONE_ENDPOINT = "https://api.typesafe.ai/v1/systemone
18
18
 
19
19
  /** Stable alias default. An exact supported version may be pinned through config. */
20
20
  export const DEFAULT_SELECTOR_MODEL = "jev-latest";
21
- /** Environment variable that holds the TypeSafe credential. Never the credential value. */
22
- export const DEFAULT_API_KEY_ENV = "TYPESAFE_API_KEY";
23
21
  /** Default logical selection deadline in milliseconds. */
24
22
  export const DEFAULT_ROUTING_TIMEOUT_MS = 15_000;
25
23
  export const ROUTING_TIMEOUT_MIN_MS = 100;
@@ -86,11 +84,13 @@ export interface JevRoutingModelEntry {
86
84
 
87
85
  /**
88
86
  * Immutable `jevRouting` snapshot parsed from the user config subtree.
89
- * The credential value never appears here only the environment-variable name.
87
+ * Contains a private credential. Never log this snapshot or serialize it into prompts,
88
+ * selector bodies, task specs, receipts or results; use explicit non-secret projections.
90
89
  */
91
90
  export interface JevRoutingConfig {
92
91
  readonly selectorModel: string;
93
- readonly apiKeyEnv: string;
92
+ /** User-configured TypeSafe credential; transport Authorization header only. */
93
+ readonly apiKey: string;
94
94
  readonly timeoutMs: number;
95
95
  readonly models: readonly JevRoutingModelEntry[];
96
96
  }