@sars267/pi-delegation-broker 0.1.0-alpha.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/SECURITY.md +19 -0
- package/docs/INTEGRATION.md +23 -0
- package/package.json +53 -0
- package/src/broker.mjs +420 -0
- package/src/fake-provider.mjs +81 -0
- package/src/index.mjs +11 -0
- package/src/ipc.mjs +249 -0
- package/src/isolated-child-config.mjs +27 -0
- package/src/signed-registry.mjs +154 -0
- package/src/supervisor.mjs +278 -0
- package/src/testing.mjs +3 -0
- package/src/trusted-launch-resolver.mjs +194 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are documented here. This project follows [Semantic Versioning](https://semver.org/) after `1.0.0`.
|
|
4
|
+
|
|
5
|
+
## 0.1.0-alpha.0 - 2026-08-15
|
|
6
|
+
|
|
7
|
+
Initial public research release.
|
|
8
|
+
|
|
9
|
+
- Single-host SQLite lease broker with fencing, TTL, shared capacity and redacted ledger.
|
|
10
|
+
- Owner-only Unix-socket IPC with controller and child-scoped authority.
|
|
11
|
+
- Signed Ed25519 registry verification and supervisor lifecycle reference.
|
|
12
|
+
- Controller-only brokered launch resolver and deterministic fake transport.
|
|
13
|
+
- No real provider proxy, credential storage, account discovery or multi-host coordination.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vitalii Simko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @sars267/pi-delegation-broker
|
|
2
|
+
|
|
3
|
+
> **Alpha / research reference — do not connect it to a real provider account or credential.**
|
|
4
|
+
|
|
5
|
+
A single-host Node.js reference implementation for capability-scoped Pi child delegation. It demonstrates a safe control path:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
trusted controller → signed registry → SQLite lease broker → scoped Unix-socket capability → isolated child policy
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It is designed to fail closed when a provider transport is not explicitly supplied. The package ships **no** HTTP client, OAuth integration, credential store, account rotation, real model proxy, or automatic provider failover.
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
This package is publishable as an **`alpha` research artifact**, not a production broker. Its API, file schema and lifecycle hooks may change before `1.0.0`.
|
|
16
|
+
|
|
17
|
+
Validated locally on Node 26 with deterministic fake transport only. The fake transport is opt-in from `@sars267/pi-delegation-broker/testing` and must never be wired to a real model route.
|
|
18
|
+
|
|
19
|
+
## What it provides
|
|
20
|
+
|
|
21
|
+
- SQLite (`BEGIN IMMEDIATE` + WAL) lease reservation, TTL, fencing and shared capacity groups.
|
|
22
|
+
- SHA-256-hashed lease capabilities for owner-only Unix-socket IPC.
|
|
23
|
+
- Signed Ed25519 capability-registry verification before supervisor startup.
|
|
24
|
+
- Owner-only state directory, lock, socket, bounded shutdown and periodic TTL sweep.
|
|
25
|
+
- A controller-only resolver that checks the exact approved model before creating a child policy.
|
|
26
|
+
- Pre-handoff and post-session cleanup hooks for a compatible patched child launcher.
|
|
27
|
+
- Deterministic test fixtures for success, 429, expiry, cancellation and stale capability scenarios.
|
|
28
|
+
|
|
29
|
+
## What it deliberately does **not** provide
|
|
30
|
+
|
|
31
|
+
- A real provider proxy, streaming upstream API calls, credential vault or account discovery.
|
|
32
|
+
- Multi-host coordination, automatic stale-lock recovery, production daemon/service management or dashboard.
|
|
33
|
+
- Signing-key storage, rotation, revocation or registry distribution.
|
|
34
|
+
- A stable upstream Pi launcher API. The [integration patch](https://github.com/Sarrius/pi-delegation-broker/tree/main/patches) is source-repository-only and is **not** installed by this package.
|
|
35
|
+
- A sandbox. Worktrees and a minimal environment are not operating-system isolation.
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Node.js **26 or later**. The package uses the built-in `node:sqlite` API.
|
|
40
|
+
- macOS/Linux Unix-domain sockets for the supervisor IPC path.
|
|
41
|
+
- For Pi child integration: a separately reviewed/accepted launcher seam. Do not patch global Pi files automatically.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
The first public prerelease should use the `next` npm tag:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npm install @sars267/pi-delegation-broker@next
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Do not install from npm until the repository release checklist is complete and the package name is confirmed available.
|
|
52
|
+
|
|
53
|
+
## Minimal API shape
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import {
|
|
57
|
+
SingleHostBrokerSupervisor,
|
|
58
|
+
BrokeredLaunchResolver,
|
|
59
|
+
verifySignedRegistry,
|
|
60
|
+
} from "@sars267/pi-delegation-broker";
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A supervisor requires a signed registry by default. Passing an unsigned fixture requires the explicit `allowUnsignedFixture: true` escape hatch, which exists only for deterministic tests.
|
|
64
|
+
|
|
65
|
+
The controller owns all of these values:
|
|
66
|
+
|
|
67
|
+
- signed registry and trusted public-key keyring;
|
|
68
|
+
- controller token;
|
|
69
|
+
- approved task contract and exact provider/model;
|
|
70
|
+
- child-agent root and explicit extension allowlist.
|
|
71
|
+
|
|
72
|
+
A child receives only a short-lived lease capability. Never pass a controller token, registry signing key, OAuth token, API key or complete account inventory to child environment, prompt, result or log.
|
|
73
|
+
|
|
74
|
+
See [`docs/INTEGRATION.md`](docs/INTEGRATION.md) for the intended boundary and [`RELEASE.md`](RELEASE.md) before publishing.
|
|
75
|
+
|
|
76
|
+
## Verification
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
npm ci
|
|
80
|
+
npm run release:check
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This runs syntax checks, deterministic tests and an `npm pack --dry-run` manifest/secret guard. It does **not** run live provider tests.
|
|
84
|
+
|
|
85
|
+
## Security
|
|
86
|
+
|
|
87
|
+
Please read [`SECURITY.md`](SECURITY.md). Do not open public issues containing tokens, account identifiers, database copies or provider responses.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
[MIT](LICENSE)
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Only the latest prerelease is supported while this project remains `0.x`.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Do **not** open a public issue with any of the following:
|
|
10
|
+
|
|
11
|
+
- provider/API/OAuth credentials or refresh tokens;
|
|
12
|
+
- `auth.json`, SQLite database, broker socket, lease capability or controller token;
|
|
13
|
+
- account identifiers, raw prompts, provider responses or signed private keys.
|
|
14
|
+
|
|
15
|
+
Use the repository's private GitHub Security Advisory reporting flow. If that flow is unavailable, open a minimal public issue requesting a private contact channel and include no sensitive material.
|
|
16
|
+
|
|
17
|
+
## Scope warning
|
|
18
|
+
|
|
19
|
+
This is an alpha reference implementation. It intentionally has no real provider transport or credential storage. Do not treat its test-only fake transport, local Unix socket, lock file or signed-registry test keys as a production security boundary.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Pi launcher integration boundary
|
|
2
|
+
|
|
3
|
+
This package does not install itself into Pi and does not modify Pi configuration.
|
|
4
|
+
|
|
5
|
+
A compatible launcher must enforce all of the following before a brokered child starts:
|
|
6
|
+
|
|
7
|
+
1. Call a controller-owned trusted resolver before worktree, shim or subprocess creation.
|
|
8
|
+
2. Give the resolver only a prompt digest and resolved request facts, not raw prompt text.
|
|
9
|
+
3. Let the resolver reserve a lease, verify the exact provider/model, create an owner-only agent directory and return a policy.
|
|
10
|
+
4. Start the child with minimal inherited environment, isolated `PI_CODING_AGENT_DIR`, explicit extensions and model scope.
|
|
11
|
+
5. Pass only `PI_BROKER_SOCKET`, lease metadata and `PI_BROKER_CAPABILITY` to the child.
|
|
12
|
+
6. Invoke parent-only hooks after child-session handoff, before-handoff failure and session-process closure.
|
|
13
|
+
|
|
14
|
+
The [source-repository-only reference patch](https://github.com/Sarrius/pi-delegation-broker/blob/main/patches/pi-subagent-workflow-trusted-launcher-seam.patch) demonstrates such a seam against `pi-subagent-workflow` commit `0c28ce87bc45f4c3d66e0100b58ae13cf345978c`. It is an unaccepted local patch and is not part of the npm tarball. Applying it, accepting it upstream, or replacing it with an equivalent stable API remains a human release decision.
|
|
15
|
+
|
|
16
|
+
`pi-multi-account` must be absent from brokered children until it has a reviewed brokered report-only mode. A brokered child must not invoke `pi.setModel()` or auto-continue on its own.
|
|
17
|
+
|
|
18
|
+
## Failure semantics
|
|
19
|
+
|
|
20
|
+
- Before handoff: controller release cleans up the lease/agent directory. If controller IPC fails, the resolver retains an explicitly pending record for later reconciliation.
|
|
21
|
+
- After handoff: child provider shutdown releases its lease; parent completion cleanup is idempotent.
|
|
22
|
+
- Provider transport missing: `BrokerIpcServer` returns `provider_transport_unavailable`; it never falls back to fake success.
|
|
23
|
+
- A real provider proxy must classify and reconcile any ambiguous external effect. The fake transport only proves pre-effect cancellation.
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sars267/pi-delegation-broker",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Non-production, single-host reference broker for capability-scoped Pi child delegation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.mjs",
|
|
9
|
+
"./testing": "./src/testing.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"docs/INTEGRATION.md",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"SECURITY.md",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=26.0.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"check": "node --check src/*.mjs && node --check test/*.mjs && node scripts/check-public-tree.mjs",
|
|
24
|
+
"test": "node --test test/*.test.mjs",
|
|
25
|
+
"pack:check": "node scripts/check-package.mjs",
|
|
26
|
+
"release:check": "npm run check && npm test && npm run pack:check",
|
|
27
|
+
"prepublishOnly": "npm run release:check"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"pi",
|
|
31
|
+
"agent",
|
|
32
|
+
"delegation",
|
|
33
|
+
"broker",
|
|
34
|
+
"lease",
|
|
35
|
+
"capability",
|
|
36
|
+
"sqlite",
|
|
37
|
+
"security"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"author": "Vitalii Simko",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/Sarrius/pi-delegation-broker.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/Sarrius/pi-delegation-broker/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/Sarrius/pi-delegation-broker#readme",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public",
|
|
51
|
+
"tag": "next"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/broker.mjs
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
|
|
4
|
+
const ENFORCEMENT = Object.freeze({ hard: 2, metered_best_effort: 1, unavailable: 0 });
|
|
5
|
+
const SECRET_PATTERNS = [
|
|
6
|
+
/sk-[A-Za-z0-9_-]{8,}/g,
|
|
7
|
+
/(?:api[_-]?key|token|authorization)\s*[:=]\s*[^\s,;]+/gi,
|
|
8
|
+
/CANARY_SECRET_[A-Za-z0-9_-]+/g,
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function redact(value) {
|
|
12
|
+
if (typeof value === "string") return SECRET_PATTERNS.reduce((text, pattern) => text.replace(pattern, "[REDACTED]"), value);
|
|
13
|
+
if (Array.isArray(value)) return value.map(redact);
|
|
14
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, redact(entry)]));
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseJson(value) { return JSON.parse(value); }
|
|
19
|
+
|
|
20
|
+
function capabilityDigest(capability) {
|
|
21
|
+
return createHash("sha256").update(capability).digest("hex");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function enforcementSatisfies(actual, required) {
|
|
25
|
+
return Object.hasOwn(ENFORCEMENT, actual)
|
|
26
|
+
&& Object.hasOwn(ENFORCEMENT, required)
|
|
27
|
+
&& ENFORCEMENT[actual] >= ENFORCEMENT[required];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function asLease(row) {
|
|
31
|
+
return {
|
|
32
|
+
leaseId: row.lease_id,
|
|
33
|
+
taskId: row.task_id,
|
|
34
|
+
resourceId: row.resource_id,
|
|
35
|
+
capacityGroup: row.capacity_group,
|
|
36
|
+
profile: row.profile,
|
|
37
|
+
fencingToken: row.fencing_token,
|
|
38
|
+
issuedAt: row.issued_at,
|
|
39
|
+
expiresAt: row.expires_at,
|
|
40
|
+
enforcement: parseJson(row.enforcement),
|
|
41
|
+
...(row.max_output_tokens === null || row.max_output_tokens === undefined ? {} : { maxOutputTokens: row.max_output_tokens }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Single-host, synchronous SQLite broker core. All capacity decisions are made
|
|
47
|
+
* in BEGIN IMMEDIATE transactions, so independent local processes sharing the
|
|
48
|
+
* same database file serialize reserve/release/fencing transitions.
|
|
49
|
+
*
|
|
50
|
+
* It deliberately has no HTTP/RPC provider client and holds no credentials.
|
|
51
|
+
*/
|
|
52
|
+
export class SqliteLeaseBroker {
|
|
53
|
+
#db;
|
|
54
|
+
|
|
55
|
+
constructor({ path, registry }) {
|
|
56
|
+
if (!path) throw new Error("SQLite broker needs a database path");
|
|
57
|
+
this.#db = new DatabaseSync(path);
|
|
58
|
+
// Configure the connection wait before contending for the database-wide
|
|
59
|
+
// journal-mode lock during independent-process startup.
|
|
60
|
+
this.#db.exec("PRAGMA busy_timeout = 5000;");
|
|
61
|
+
this.#db.exec("PRAGMA foreign_keys = ON;");
|
|
62
|
+
this.#db.exec("PRAGMA journal_mode = WAL;");
|
|
63
|
+
this.#migrate();
|
|
64
|
+
this.#seed(registry);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
close() { this.#db.close(); }
|
|
68
|
+
|
|
69
|
+
reserve(contract, now) {
|
|
70
|
+
return this.#transaction(() => {
|
|
71
|
+
this.#expire(now);
|
|
72
|
+
const policyError = this.#validateContract(contract);
|
|
73
|
+
if (policyError) {
|
|
74
|
+
this.#record(now, "ReservationDenied", { taskId: contract?.taskId, status: "denied_policy", reason: policyError });
|
|
75
|
+
return { status: "denied_policy", reasons: [policyError] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const requestedProfile = this.#profile(contract.capability.minimumProfile);
|
|
79
|
+
const rows = this.#db.prepare(`
|
|
80
|
+
SELECT r.id, r.capacity_group, r.profile, r.state, r.cooldown_until, r.enforcement, g.max_concurrent
|
|
81
|
+
FROM resources r JOIN capacity_groups g ON g.id = r.capacity_group
|
|
82
|
+
ORDER BY r.id
|
|
83
|
+
`).all();
|
|
84
|
+
let earliestCompatibleAt;
|
|
85
|
+
for (const resource of rows) {
|
|
86
|
+
if (resource.state !== "healthy") continue;
|
|
87
|
+
if (resource.cooldown_until > now) {
|
|
88
|
+
if (this.#resourceMatchesContract(resource, requestedProfile, contract)) {
|
|
89
|
+
earliestCompatibleAt = earliestCompatibleAt === undefined ? resource.cooldown_until : Math.min(earliestCompatibleAt, resource.cooldown_until);
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!this.#resourceMatchesContract(resource, requestedProfile, contract)) continue;
|
|
94
|
+
const active = this.#db.prepare("SELECT count(*) AS count FROM leases WHERE capacity_group = ? AND expires_at > ?")
|
|
95
|
+
.get(resource.capacity_group, now).count;
|
|
96
|
+
if (active >= resource.max_concurrent) continue;
|
|
97
|
+
|
|
98
|
+
const fenceRow = this.#db.prepare("UPDATE capacity_groups SET fencing_token = fencing_token + 1 WHERE id = ? RETURNING fencing_token")
|
|
99
|
+
.get(resource.capacity_group);
|
|
100
|
+
const lease = {
|
|
101
|
+
leaseId: randomUUID(),
|
|
102
|
+
taskId: contract.taskId,
|
|
103
|
+
resourceId: resource.id,
|
|
104
|
+
capacityGroup: resource.capacity_group,
|
|
105
|
+
profile: resource.profile,
|
|
106
|
+
fencingToken: fenceRow.fencing_token,
|
|
107
|
+
issuedAt: now,
|
|
108
|
+
expiresAt: now + (contract.leaseTtlMs ?? 30_000),
|
|
109
|
+
enforcement: parseJson(resource.enforcement),
|
|
110
|
+
...(Number.isInteger(contract.budget?.maxOutputTokens) ? { maxOutputTokens: contract.budget.maxOutputTokens } : {}),
|
|
111
|
+
};
|
|
112
|
+
this.#db.prepare(`
|
|
113
|
+
INSERT INTO leases (lease_id, task_id, resource_id, capacity_group, profile, fencing_token, issued_at, expires_at, enforcement, max_output_tokens)
|
|
114
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
115
|
+
`).run(
|
|
116
|
+
lease.leaseId, lease.taskId, lease.resourceId, lease.capacityGroup, lease.profile,
|
|
117
|
+
lease.fencingToken, lease.issuedAt, lease.expiresAt, JSON.stringify(lease.enforcement), lease.maxOutputTokens ?? null,
|
|
118
|
+
);
|
|
119
|
+
this.#record(now, "LeaseIssued", lease);
|
|
120
|
+
return { status: "leased", lease };
|
|
121
|
+
}
|
|
122
|
+
const result = { status: "denied_capacity", ...(earliestCompatibleAt === undefined ? {} : { earliestCompatibleAt }) };
|
|
123
|
+
this.#record(now, "ReservationDenied", { taskId: contract.taskId, ...result });
|
|
124
|
+
return result;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
heartbeat(leaseId, fencingToken, now, ttlMs = 30_000) {
|
|
129
|
+
return this.#transaction(() => {
|
|
130
|
+
const update = this.#db.prepare("UPDATE leases SET expires_at = ? WHERE lease_id = ? AND fencing_token = ? AND expires_at > ?")
|
|
131
|
+
.run(now + ttlMs, leaseId, fencingToken, now);
|
|
132
|
+
if (update.changes !== 1) return { status: "denied_lease" };
|
|
133
|
+
const lease = asLease(this.#db.prepare("SELECT * FROM leases WHERE lease_id = ?").get(leaseId));
|
|
134
|
+
this.#record(now, "LeaseHeartbeated", { leaseId, fencingToken, expiresAt: lease.expiresAt });
|
|
135
|
+
return { status: "leased", lease };
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Issue a least-privilege IPC capability after a controller has reserved a
|
|
141
|
+
* lease. Only its SHA-256 digest is persisted; the raw capability is never
|
|
142
|
+
* included in audit events and is invalidated by lease release/expiry.
|
|
143
|
+
*/
|
|
144
|
+
issueLeaseCapability(leaseId, fencingToken, now) {
|
|
145
|
+
return this.#transaction(() => {
|
|
146
|
+
const lease = this.#db.prepare("SELECT * FROM leases WHERE lease_id = ? AND fencing_token = ? AND expires_at > ?")
|
|
147
|
+
.get(leaseId, fencingToken, now);
|
|
148
|
+
if (!lease) return { status: "denied_lease" };
|
|
149
|
+
const capability = randomBytes(32).toString("base64url");
|
|
150
|
+
this.#db.prepare("DELETE FROM lease_capabilities WHERE lease_id = ?").run(leaseId);
|
|
151
|
+
this.#db.prepare(`
|
|
152
|
+
INSERT INTO lease_capabilities (capability_hash, lease_id, fencing_token, expires_at)
|
|
153
|
+
VALUES (?, ?, ?, ?)
|
|
154
|
+
`).run(capabilityDigest(capability), leaseId, fencingToken, lease.expires_at);
|
|
155
|
+
this.#record(now, "LeaseCapabilityIssued", { leaseId, fencingToken, expiresAt: lease.expires_at });
|
|
156
|
+
return { status: "issued", capability, expiresAt: lease.expires_at };
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Validate a child-scoped IPC capability without exposing controller authority. */
|
|
161
|
+
leaseForCapability(capability, now) {
|
|
162
|
+
if (typeof capability !== "string" || !capability) return { status: "denied_capability" };
|
|
163
|
+
const row = this.#db.prepare(`
|
|
164
|
+
SELECT l.*
|
|
165
|
+
FROM lease_capabilities c JOIN leases l ON l.lease_id = c.lease_id
|
|
166
|
+
WHERE c.capability_hash = ?
|
|
167
|
+
AND c.fencing_token = l.fencing_token
|
|
168
|
+
AND c.expires_at > ?
|
|
169
|
+
AND l.expires_at > ?
|
|
170
|
+
`).get(capabilityDigest(capability), now, now);
|
|
171
|
+
return row ? { status: "authorized", lease: asLease(row) } : { status: "denied_capability" };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Record already-classified provider telemetry without retaining raw child input. */
|
|
175
|
+
recordProviderEvent(leaseId, fencingToken, inputDigest, event, now) {
|
|
176
|
+
return this.#transaction(() => {
|
|
177
|
+
const lease = this.#db.prepare("SELECT * FROM leases WHERE lease_id = ? AND fencing_token = ? AND expires_at > ?")
|
|
178
|
+
.get(leaseId, fencingToken, now);
|
|
179
|
+
if (!lease) return { status: "denied_lease" };
|
|
180
|
+
this.#record(now, "ProviderEvent", { leaseId, fencingToken, inputDigest, event });
|
|
181
|
+
return { status: "recorded", lease: asLease(lease) };
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
release(leaseId, fencingToken, reason, now) {
|
|
186
|
+
return this.#transaction(() => {
|
|
187
|
+
const deleted = this.#db.prepare("DELETE FROM leases WHERE lease_id = ? AND fencing_token = ?").run(leaseId, fencingToken);
|
|
188
|
+
if (deleted.changes !== 1) return { status: "denied_lease" };
|
|
189
|
+
this.#record(now, "LeaseReleased", { leaseId, fencingToken, reason });
|
|
190
|
+
return { status: "released" };
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
expire(now) {
|
|
195
|
+
return this.#transaction(() => this.#expire(now));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
markRateLimited(resourceId, retryAfterMs, now) {
|
|
199
|
+
return this.#transaction(() => {
|
|
200
|
+
const resource = this.#db.prepare("SELECT capacity_group FROM resources WHERE id = ?").get(resourceId);
|
|
201
|
+
if (!resource) throw new Error(`Unknown resource ${resourceId}`);
|
|
202
|
+
const until = now + Math.max(1, Number.isFinite(retryAfterMs) ? retryAfterMs : 60_000);
|
|
203
|
+
this.#db.prepare("UPDATE resources SET cooldown_until = MAX(cooldown_until, ?) WHERE capacity_group = ?")
|
|
204
|
+
.run(until, resource.capacity_group);
|
|
205
|
+
this.#record(now, "CapacityGroupCooldown", { resourceId, capacityGroup: resource.capacity_group, until });
|
|
206
|
+
return { status: "cooling_down", capacityGroup: resource.capacity_group, until };
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
markUnknown(resourceId, now, reason = "unknown") {
|
|
211
|
+
return this.#transaction(() => {
|
|
212
|
+
const update = this.#db.prepare("UPDATE resources SET state = 'unknown' WHERE id = ?").run(resourceId);
|
|
213
|
+
if (update.changes !== 1) throw new Error(`Unknown resource ${resourceId}`);
|
|
214
|
+
this.#record(now, "ResourceUnknown", { resourceId, reason });
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
markHealthy(resourceId, now) {
|
|
219
|
+
return this.#transaction(() => {
|
|
220
|
+
const update = this.#db.prepare("UPDATE resources SET state = 'healthy' WHERE id = ?").run(resourceId);
|
|
221
|
+
if (update.changes !== 1) throw new Error(`Unknown resource ${resourceId}`);
|
|
222
|
+
this.#record(now, "ResourceHealthy", { resourceId });
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
authorizeEffect(intent, now) {
|
|
227
|
+
const lease = this.#db.prepare("SELECT * FROM leases WHERE lease_id = ?").get(intent.leaseId);
|
|
228
|
+
if (!lease || lease.fencing_token !== intent.fencingToken || lease.expires_at <= now || lease.task_id !== intent.taskId) {
|
|
229
|
+
return { status: "denied_lease" };
|
|
230
|
+
}
|
|
231
|
+
if (!intent.approval || intent.approval.expiresAt <= now || intent.approval.targetDigest !== intent.targetDigest) {
|
|
232
|
+
return { status: "denied_policy", reasons: ["effect requires a live approval bound to targetDigest"] };
|
|
233
|
+
}
|
|
234
|
+
if (typeof intent.idempotencyKey !== "string" || !intent.idempotencyKey) {
|
|
235
|
+
return { status: "denied_policy", reasons: ["effect requires an idempotency key"] };
|
|
236
|
+
}
|
|
237
|
+
return { status: "authorized", lease: asLease(lease) };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Durable idempotency ledger boundary; it does not perform an external effect. */
|
|
241
|
+
recordEffect(intent, now) {
|
|
242
|
+
return this.#transaction(() => {
|
|
243
|
+
const authorization = this.authorizeEffect(intent, now);
|
|
244
|
+
if (authorization.status !== "authorized") return authorization;
|
|
245
|
+
const existing = this.#db.prepare("SELECT receipt FROM effect_receipts WHERE idempotency_key = ?").get(intent.idempotencyKey);
|
|
246
|
+
if (existing) return { status: "replayed", receipt: parseJson(existing.receipt) };
|
|
247
|
+
const receipt = { receiptId: randomUUID(), idempotencyKey: intent.idempotencyKey, targetDigest: intent.targetDigest, recordedAt: now };
|
|
248
|
+
this.#db.prepare("INSERT INTO effect_receipts (idempotency_key, receipt) VALUES (?, ?)")
|
|
249
|
+
.run(intent.idempotencyKey, JSON.stringify(receipt));
|
|
250
|
+
this.#record(now, "EffectRecorded", { taskId: intent.taskId, leaseId: intent.leaseId, idempotencyKey: intent.idempotencyKey, targetDigest: intent.targetDigest });
|
|
251
|
+
return { status: "recorded", receipt };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
leases() {
|
|
256
|
+
return this.#db.prepare("SELECT * FROM leases ORDER BY issued_at, lease_id").all().map(asLease);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
events() {
|
|
260
|
+
return this.#db.prepare("SELECT sequence, at, type, payload FROM events ORDER BY sequence").all()
|
|
261
|
+
.map((row) => ({ sequence: row.sequence, at: row.at, type: row.type, payload: parseJson(row.payload) }));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#migrate() {
|
|
265
|
+
this.#db.exec(`
|
|
266
|
+
CREATE TABLE IF NOT EXISTS profiles (
|
|
267
|
+
id TEXT PRIMARY KEY,
|
|
268
|
+
status TEXT NOT NULL,
|
|
269
|
+
capabilities TEXT NOT NULL
|
|
270
|
+
);
|
|
271
|
+
CREATE TABLE IF NOT EXISTS capacity_groups (
|
|
272
|
+
id TEXT PRIMARY KEY,
|
|
273
|
+
max_concurrent INTEGER NOT NULL CHECK (max_concurrent > 0),
|
|
274
|
+
fencing_token INTEGER NOT NULL DEFAULT 0
|
|
275
|
+
);
|
|
276
|
+
CREATE TABLE IF NOT EXISTS resources (
|
|
277
|
+
id TEXT PRIMARY KEY,
|
|
278
|
+
capacity_group TEXT NOT NULL REFERENCES capacity_groups(id),
|
|
279
|
+
profile TEXT NOT NULL REFERENCES profiles(id),
|
|
280
|
+
state TEXT NOT NULL CHECK (state IN ('healthy', 'unknown')) DEFAULT 'healthy',
|
|
281
|
+
cooldown_until INTEGER NOT NULL DEFAULT 0,
|
|
282
|
+
enforcement TEXT NOT NULL
|
|
283
|
+
);
|
|
284
|
+
CREATE TABLE IF NOT EXISTS leases (
|
|
285
|
+
lease_id TEXT PRIMARY KEY,
|
|
286
|
+
task_id TEXT NOT NULL,
|
|
287
|
+
resource_id TEXT NOT NULL REFERENCES resources(id),
|
|
288
|
+
capacity_group TEXT NOT NULL REFERENCES capacity_groups(id),
|
|
289
|
+
profile TEXT NOT NULL,
|
|
290
|
+
fencing_token INTEGER NOT NULL,
|
|
291
|
+
issued_at INTEGER NOT NULL,
|
|
292
|
+
expires_at INTEGER NOT NULL,
|
|
293
|
+
enforcement TEXT NOT NULL,
|
|
294
|
+
max_output_tokens INTEGER
|
|
295
|
+
);
|
|
296
|
+
CREATE INDEX IF NOT EXISTS leases_by_group_expiry ON leases(capacity_group, expires_at);
|
|
297
|
+
CREATE TABLE IF NOT EXISTS lease_capabilities (
|
|
298
|
+
capability_hash TEXT PRIMARY KEY,
|
|
299
|
+
lease_id TEXT NOT NULL UNIQUE REFERENCES leases(lease_id) ON DELETE CASCADE,
|
|
300
|
+
fencing_token INTEGER NOT NULL,
|
|
301
|
+
expires_at INTEGER NOT NULL
|
|
302
|
+
);
|
|
303
|
+
CREATE TABLE IF NOT EXISTS effect_receipts (
|
|
304
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
305
|
+
receipt TEXT NOT NULL
|
|
306
|
+
);
|
|
307
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
308
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
309
|
+
at INTEGER NOT NULL,
|
|
310
|
+
type TEXT NOT NULL,
|
|
311
|
+
payload TEXT NOT NULL
|
|
312
|
+
);
|
|
313
|
+
`);
|
|
314
|
+
// Existing development databases predate max_output_tokens. Keep this
|
|
315
|
+
// additive migration explicit rather than silently treating an old lease
|
|
316
|
+
// as a hard-capped one.
|
|
317
|
+
const columns = this.#db.prepare("PRAGMA table_info(leases)").all().map((column) => column.name);
|
|
318
|
+
if (!columns.includes("max_output_tokens")) this.#db.exec("ALTER TABLE leases ADD COLUMN max_output_tokens INTEGER");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#seed(registry) {
|
|
322
|
+
if (!registry?.profiles || !registry?.capacityGroups || !registry?.resources) throw new Error("Broker registry needs profiles, capacityGroups, and resources");
|
|
323
|
+
this.#transaction(() => {
|
|
324
|
+
for (const [id, profile] of Object.entries(registry.profiles)) {
|
|
325
|
+
this.#db.prepare("INSERT OR IGNORE INTO profiles (id, status, capabilities) VALUES (?, ?, ?)")
|
|
326
|
+
.run(id, profile.status, JSON.stringify(profile.supports));
|
|
327
|
+
}
|
|
328
|
+
for (const [id, group] of Object.entries(registry.capacityGroups)) {
|
|
329
|
+
this.#db.prepare("INSERT OR IGNORE INTO capacity_groups (id, max_concurrent) VALUES (?, ?)").run(id, group.maxConcurrent);
|
|
330
|
+
}
|
|
331
|
+
for (const [id, resource] of Object.entries(registry.resources)) {
|
|
332
|
+
this.#db.prepare(`
|
|
333
|
+
INSERT OR IGNORE INTO resources (id, capacity_group, profile, state, cooldown_until, enforcement)
|
|
334
|
+
VALUES (?, ?, ?, 'healthy', 0, ?)
|
|
335
|
+
`).run(id, resource.capacityGroup, resource.profile, JSON.stringify(resource.enforcement));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#transaction(fn) {
|
|
341
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
342
|
+
try {
|
|
343
|
+
const result = fn();
|
|
344
|
+
this.#db.exec("COMMIT");
|
|
345
|
+
return result;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
try { this.#db.exec("ROLLBACK"); } catch { /* transaction may already be closed */ }
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#expire(now) {
|
|
353
|
+
const rows = this.#db.prepare("SELECT lease_id, fencing_token FROM leases WHERE expires_at <= ?").all(now);
|
|
354
|
+
if (rows.length) {
|
|
355
|
+
this.#db.prepare("DELETE FROM leases WHERE expires_at <= ?").run(now);
|
|
356
|
+
for (const row of rows) this.#record(now, "LeaseExpired", { leaseId: row.lease_id, fencingToken: row.fencing_token });
|
|
357
|
+
}
|
|
358
|
+
return rows.map((row) => row.lease_id);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#validateContract(contract) {
|
|
362
|
+
if (!contract?.taskId || !contract?.capability?.minimumProfile || !Array.isArray(contract.capability.required)) return "invalid contract";
|
|
363
|
+
if (contract.capability.downgradePolicy !== "forbid") return "only forbid downgrade policy is implemented in broker MVP";
|
|
364
|
+
if (!this.#profile(contract.capability.minimumProfile)) return "unknown or unapproved minimum profile";
|
|
365
|
+
const requirements = contract.budget?.enforcement ?? {};
|
|
366
|
+
if (Object.values(requirements).some((value) => !Object.hasOwn(ENFORCEMENT, value))) return "unknown budget enforcement class";
|
|
367
|
+
if ((contract.operationClass === "apply" || contract.operationClass === "external_write")
|
|
368
|
+
&& Object.values(requirements).some((value) => value !== "hard")) {
|
|
369
|
+
return "high-risk task requires hard budget enforcement for every declared dimension";
|
|
370
|
+
}
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#profile(id) {
|
|
375
|
+
const profile = this.#db.prepare("SELECT id, status, capabilities FROM profiles WHERE id = ?").get(id);
|
|
376
|
+
return !profile || profile.status !== "approved" ? undefined : { id: profile.id, capabilities: parseJson(profile.capabilities) };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#resourceMatchesContract(resource, requestedProfile, contract) {
|
|
380
|
+
if (!requestedProfile || resource.profile !== requestedProfile.id) return false;
|
|
381
|
+
if (!contract.capability.required.every((capability) => requestedProfile.capabilities.includes(capability))) return false;
|
|
382
|
+
const enforcement = parseJson(resource.enforcement);
|
|
383
|
+
return Object.entries(contract.budget?.enforcement ?? {}).every(([dimension, required]) => enforcementSatisfies(enforcement[dimension], required));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#record(at, type, payload) {
|
|
387
|
+
this.#db.prepare("INSERT INTO events (at, type, payload) VALUES (?, ?, ?)").run(at, type, JSON.stringify(redact(payload)));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function fixtureRegistry() {
|
|
392
|
+
return {
|
|
393
|
+
profiles: {
|
|
394
|
+
"reasoning-high/v1": { status: "approved", supports: ["code_reasoning", "repo_navigation"] },
|
|
395
|
+
"audit-low/v1": { status: "approved", supports: ["read_only_audit"] },
|
|
396
|
+
},
|
|
397
|
+
capacityGroups: {
|
|
398
|
+
"G-shared": { maxConcurrent: 1 },
|
|
399
|
+
"G-independent": { maxConcurrent: 1 },
|
|
400
|
+
"G-cheap": { maxConcurrent: 4 },
|
|
401
|
+
},
|
|
402
|
+
resources: {
|
|
403
|
+
R1: { capacityGroup: "G-shared", profile: "reasoning-high/v1", enforcement: { input: "hard", output: "hard", cost: "metered_best_effort" } },
|
|
404
|
+
R1_ALIAS: { capacityGroup: "G-shared", profile: "reasoning-high/v1", enforcement: { input: "hard", output: "hard", cost: "metered_best_effort" } },
|
|
405
|
+
R2: { capacityGroup: "G-independent", profile: "reasoning-high/v1", enforcement: { input: "hard", output: "hard", cost: "metered_best_effort" } },
|
|
406
|
+
R3: { capacityGroup: "G-cheap", profile: "audit-low/v1", enforcement: { input: "unavailable", output: "unavailable", cost: "unavailable" } },
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function fixtureContract(overrides = {}) {
|
|
412
|
+
return {
|
|
413
|
+
taskId: "task-1",
|
|
414
|
+
operationClass: "observe",
|
|
415
|
+
capability: { minimumProfile: "reasoning-high/v1", required: ["code_reasoning", "repo_navigation"], downgradePolicy: "forbid" },
|
|
416
|
+
budget: { maxOutputTokens: 100, enforcement: { input: "hard", output: "hard", cost: "metered_best_effort" } },
|
|
417
|
+
leaseTtlMs: 30_000,
|
|
418
|
+
...overrides,
|
|
419
|
+
};
|
|
420
|
+
}
|