@openparachute/agent
Advanced tools
@@ -56,3 +56,3 @@ { | ||
| "tags": ["agent/message/inbound"], | ||
| "has_metadata": ["channel"], | ||
| "has_metadata": ["agent"], | ||
| "missing_metadata": ["channel_inbound_rendered_at"] | ||
@@ -59,0 +59,0 @@ } |
+1
-1
| { | ||
| "name": "@openparachute/agent", | ||
| "version": "0.2.4", | ||
| "version": "0.2.5-rc.1", | ||
| "description": "Vault-native agents for Claude Code — a #agent/definition note + an inbound message becomes a sandboxed claude turn; the reply is written back as a note. Messaging gateway on :1941.", | ||
@@ -5,0 +5,0 @@ "license": "AGPL-3.0", |
+12
-5
@@ -60,5 +60,5 @@ # Agent | ||
| Config), and vault-backed durable chat. | ||
| - **Early / changing:** the npm package isn't published yet (run from source — | ||
| tracked in [#16](https://github.com/ParachuteComputer/parachute-agent/issues/16)); | ||
| agent-session isolation is real but young; APIs may shift. | ||
| - **Early / changing:** agent-session isolation is real but young; APIs may | ||
| shift between releases (pre-1.0, small breaking changes land without a | ||
| deprecation window). | ||
@@ -89,5 +89,12 @@ **Read this about agent sessions.** A spawned agent runs `claude` with | ||
| [hub](https://github.com/ParachuteComputer/parachute-hub) is the portal + OAuth | ||
| issuer). Until the npm package ships, run it from source: | ||
| issuer). Install from npm (`@openparachute/agent` publishes via tag-triggered CI): | ||
| ```bash | ||
| parachute install agent # via the hub CLI (the normal path) | ||
| # or directly: bun add -g @openparachute/agent | ||
| ``` | ||
| For development, run it from source instead: | ||
| ```bash | ||
| git clone https://github.com/ParachuteComputer/parachute-agent | ||
@@ -106,3 +113,3 @@ cd parachute-agent | ||
| |---|---| | ||
| | npm | `@openparachute/agent` (publish pending [#16](https://github.com/ParachuteComputer/parachute-agent/issues/16)) | | ||
| | npm | [`@openparachute/agent`](https://www.npmjs.com/package/@openparachute/agent) | | ||
| | bins | `parachute-agent` (daemon), `parachute-agent-bridge` (session bridge) | | ||
@@ -109,0 +116,0 @@ | port | `1941` · paths `/agent` | |
@@ -62,8 +62,100 @@ /** | ||
| function isStringArray(v: unknown): v is string[] { | ||
| return Array.isArray(v) && v.every((x) => typeof x === "string"); | ||
| } | ||
| function isPlainObject(v: unknown): v is Record<string, unknown> { | ||
| return typeof v === "object" && v !== null && !Array.isArray(v); | ||
| } | ||
| function isDmPolicy(v: unknown): v is AccessConfig["dmPolicy"] { | ||
| return v === "open" || v === "pairing" || v === "allowlist"; | ||
| } | ||
| /** | ||
| * Shape-validate a parsed access.json object into an AccessConfig, or return | ||
| * the reason it isn't one (the caller fails closed and logs the reason). The | ||
| * GATING fields must match the schema exactly — a missing/mistyped `dmPolicy`, | ||
| * `allowFrom`, or `allowInChats` means the operator's intent can't be trusted, | ||
| * and letting it through as a bare cast would surface later as a raw TypeError | ||
| * inside `isAllowedFor` (caught by the poll loop → a retry-loop of stack | ||
| * traces) instead of a clean fail-closed. The INERT bookkeeping fields | ||
| * (`groups` / `pending` — read for official-plugin file compatibility, never | ||
| * acted on here) default to `{}` when absent: their absence can't create a | ||
| * gating hazard, so a hand-written minimal file isn't punished for omitting | ||
| * them. | ||
| */ | ||
| function toAccessConfig( | ||
| parsed: Record<string, unknown>, | ||
| ): { ok: true; access: AccessConfig } | { ok: false; reason: string } { | ||
| const { dmPolicy, allowFrom, allowInChats, groups, pending } = parsed; | ||
| if (!isDmPolicy(dmPolicy)) { | ||
| return { | ||
| ok: false, | ||
| reason: `dmPolicy must be "open" | "pairing" | "allowlist", got ${JSON.stringify(dmPolicy)}`, | ||
| }; | ||
| } | ||
| if (!isStringArray(allowFrom)) { | ||
| return { ok: false, reason: "allowFrom must be an array of strings" }; | ||
| } | ||
| if (allowInChats !== undefined && !isStringArray(allowInChats)) { | ||
| return { ok: false, reason: "allowInChats, when present, must be an array of strings" }; | ||
| } | ||
| if (groups !== undefined && !isPlainObject(groups)) { | ||
| return { ok: false, reason: "groups, when present, must be an object" }; | ||
| } | ||
| if (pending !== undefined && !isPlainObject(pending)) { | ||
| return { ok: false, reason: "pending, when present, must be an object" }; | ||
| } | ||
| return { | ||
| ok: true, | ||
| access: { dmPolicy, allowFrom, allowInChats, groups: groups ?? {}, pending: pending ?? {} }, | ||
| }; | ||
| } | ||
| /** | ||
| * Load access.json with asymmetric failure handling: | ||
| * | ||
| * - **Missing file (ENOENT)** → OPEN. A fresh install has no access.json yet; | ||
| * open-by-default matches the official plugin's out-of-the-box behavior. | ||
| * - **Existing file that can't be read, parsed, or shape-validated** → FAIL | ||
| * CLOSED. An operator who wrote an access.json expressed an intent to gate; | ||
| * silently falling back to `dmPolicy: "open"` on a corrupt file would | ||
| * disable ALL gating exactly when the operator believes it's on. We return | ||
| * an empty allowlist (nobody allowed) and log loudly so the breakage is | ||
| * visible, not silent. | ||
| */ | ||
| export function loadAccess(accessFile: string): AccessConfig { | ||
| const failClosed: AccessConfig = { dmPolicy: "allowlist", allowFrom: [], groups: {}, pending: {} }; | ||
| let raw: string; | ||
| try { | ||
| return JSON.parse(readFileSync(accessFile, "utf8")); | ||
| } catch { | ||
| return { dmPolicy: "open", allowFrom: [], groups: {}, pending: {} }; | ||
| raw = readFileSync(accessFile, "utf8"); | ||
| } catch (err) { | ||
| if ((err as NodeJS.ErrnoException)?.code === "ENOENT") { | ||
| // No access.json — fresh install, open by design. | ||
| return { dmPolicy: "open", allowFrom: [], groups: {}, pending: {} }; | ||
| } | ||
| console.error( | ||
| `parachute-agent: access.json at ${accessFile} EXISTS but could not be read — ` + | ||
| `FAILING CLOSED (allowlist, no users). Fix the file to restore access.`, | ||
| err, | ||
| ); | ||
| return failClosed; | ||
| } | ||
| try { | ||
| const parsed: unknown = JSON.parse(raw); | ||
| if (!isPlainObject(parsed)) { | ||
| throw new Error(`expected a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}`); | ||
| } | ||
| const shaped = toAccessConfig(parsed); | ||
| if (!shaped.ok) throw new Error(shaped.reason); | ||
| return shaped.access; | ||
| } catch (err) { | ||
| console.error( | ||
| `parachute-agent: access.json at ${accessFile} is corrupt (invalid JSON, or not a valid access config) — ` + | ||
| `FAILING CLOSED (allowlist, no users). Fix or delete the file to restore access.`, | ||
| err, | ||
| ); | ||
| return failClosed; | ||
| } | ||
| } | ||
@@ -70,0 +162,0 @@ |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1544961
0.27%24426
0.36%157
4.67%