loopctl-mcp-server 2.59.0 → 2.61.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/README.md +10 -4
- package/index.js +391 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -199,14 +199,14 @@ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for age
|
|
|
199
199
|
|
|
200
200
|
| Tool | Description |
|
|
201
201
|
|---|---|
|
|
202
|
-
| `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. Accepts optional `token_usage` object. |
|
|
203
|
-
| `review_complete` | Record that a review has been completed for a story. Required before verify. |
|
|
202
|
+
| `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. Accepts optional `token_usage` object. Under the LCP-1 §9.3 signed profile, pass the `claim` from `custody_sign_claim`. |
|
|
203
|
+
| `review_complete` | Record that a review has been completed for a story. Required before verify. Under the signed profile, pass the `claim` from `custody_sign_claim`. |
|
|
204
204
|
|
|
205
205
|
### Verification Tools (orchestrator key)
|
|
206
206
|
|
|
207
207
|
| Tool | Description |
|
|
208
208
|
|---|---|
|
|
209
|
-
| `verify_story` | Orchestrator verifies a reported_done story. Transitions reported_done -> verified. |
|
|
209
|
+
| `verify_story` | Orchestrator verifies a reported_done story. Transitions reported_done -> verified. Under the signed profile, pass the `claim` from `custody_sign_claim`. |
|
|
210
210
|
| `reject_story` | Orchestrator rejects a story with a reason. |
|
|
211
211
|
|
|
212
212
|
### Bulk Tools (orchestrator key)
|
|
@@ -375,7 +375,13 @@ Key distribution for the dispatch pattern (Epic 26): per-dispatch ephemeral keys
|
|
|
375
375
|
| Tool | Description |
|
|
376
376
|
|---|---|
|
|
377
377
|
| `signup` | **US-26.7.1.** Create a NEW **agent-rooted (KB-tier)** tenant and mint its one-time root API key — entirely through this call, no human operator, no hardware authenticator, no existing API key required. The tenant gets the FULL knowledge-wiki surface but **cannot** perform work-breakdown / chain-of-custody operations (those require a separate human-anchored tenant via the WebAuthn ceremony at `https://loopctl.com/signup`). Rate-limited per client IP (<= 5/hour). The `raw_key` is shown ONCE — save it immediately (e.g. as `LOOPCTL_USER_KEY`). Required: `name`, `slug`, `email`. |
|
|
378
|
-
| `dispatch` | Mint an ephemeral, scoped api_key for a sub-agent dispatch, carrying its lineage path. The `raw_key` is returned ONCE — pass it to the sub-agent's launch args, never store it in env vars; it expires after `expires_in_seconds` (default 3600, max 14400). Required: `role` (`agent`/`orchestrator`), `agent_id`. Optional: `parent_dispatch_id`, `story_id`. |
|
|
378
|
+
| `dispatch` | Mint an ephemeral, scoped api_key for a sub-agent dispatch, carrying its lineage path. The `raw_key` is returned ONCE — pass it to the sub-agent's launch args, never store it in env vars; it expires after `expires_in_seconds` (default 3600, max 14400). Required: `role` (`agent`/`orchestrator`), `agent_id`. Optional: `parent_dispatch_id`, `story_id`. **LCP-1 §9.2 signed profile:** optionally enroll an agent key via `agent_pubkey` (hex) + `alg` + an `attestation` (from `custody_sign_attestation`) + `attestation_conditions`. |
|
|
379
|
+
| `register_custody_owner_key` | **LCP-1 §9.2.** Register/rotate the tenant custody OWNER key — the root of trust the attestation chain hangs from. Private half stays with you; requires `LOOPCTL_USER_KEY` and a human-anchored tenant. Required: `owner_pubkey` (hex). ROTATION additionally requires `rotation_proof` (from `custody_sign_owner_rotation`); first registration needs none. |
|
|
380
|
+
| `list_enrolled_agent_keys` | **LCP-1 §9.1.1 transparency.** List the agent public keys enrolled under your tenant, reconstructed from the tamper-evident audit chain (not a mutable listing). Compare against the keys you generated; any excess is operator-minted. Keyset-paged (`limit`, `cursor`). |
|
|
381
|
+
| `custody_generate_keypair` | **LCP-1 §9.** Generate an Ed25519 keypair LOCALLY (private key never leaves the process). Returns `public_key_hex` + `private_key_hex`. |
|
|
382
|
+
| `custody_sign_attestation` | **LCP-1 §9.2.** Sign an attestation over an agent key to enroll it — with the OWNER private key (root, `lineage_path: []`) or the PARENT agent private key (child, `lineage_path` = parent's). Returns the hex `attestation` for `dispatch`. |
|
|
383
|
+
| `custody_sign_claim` | **LCP-1 §9.3.** Sign a custody claim with your enrolled agent private key. Returns a `claim` object to pass as the `claim` param to `report_story`/`review_complete`/`verify_story` when the deployment runs the signed profile. |
|
|
384
|
+
| `custody_sign_owner_rotation` | **LCP-1 §9.2.** Sign an owner-key ROTATION proof with the OUTGOING owner private key, proving possession before it re-roots the attestation chain. Binds the old key + its set-at (Unix microseconds) so a captured proof is not replayable after a rotate-back. Returns `rotation_proof` for `register_custody_owner_key`. |
|
|
379
385
|
| `recover_cap` | Re-mint a capability token for a story you're assigned to, after a session crash lost your cap. Required: `story_id`. Optional: `cap_type` (`start_cap`/`report_cap`, default `start_cap`), `lineage`. |
|
|
380
386
|
| `get_sth` | Get the latest Signed Tree Head for a tenant's tamper-evident audit chain. Public — no auth required. Required: `tenant_id`. |
|
|
381
387
|
| `request_authenticator_challenge` | **US-26.7.2.** Step 1 of the opt-in WebAuthn trust-tier upgrade ceremony: issues a registration challenge for enrolling a hardware authenticator against an EXISTING agent-rooted (KB-tier) tenant, promoting it to `human_anchored` on success. Requires an interactive WebAuthn client. |
|
package/index.js
CHANGED
|
@@ -951,7 +951,7 @@ async function requestReview({ story_id }) {
|
|
|
951
951
|
|
|
952
952
|
// --- Reviewer Tools (orch key — reviewer uses orchestrator role) ---
|
|
953
953
|
|
|
954
|
-
async function reportStory({ story_id, artifact_type, artifact_path, token_usage }) {
|
|
954
|
+
async function reportStory({ story_id, artifact_type, artifact_path, token_usage, claim }) {
|
|
955
955
|
const body = {};
|
|
956
956
|
if (artifact_type || artifact_path) {
|
|
957
957
|
body.artifact = {};
|
|
@@ -961,6 +961,7 @@ async function reportStory({ story_id, artifact_type, artifact_path, token_usage
|
|
|
961
961
|
if (token_usage) {
|
|
962
962
|
body.token_usage = token_usage;
|
|
963
963
|
}
|
|
964
|
+
if (claim) body.claim = claim;
|
|
964
965
|
|
|
965
966
|
const result = await apiCall(
|
|
966
967
|
"POST",
|
|
@@ -971,12 +972,13 @@ async function reportStory({ story_id, artifact_type, artifact_path, token_usage
|
|
|
971
972
|
return toContent(result);
|
|
972
973
|
}
|
|
973
974
|
|
|
974
|
-
async function reviewComplete({ story_id, review_type, findings_count, fixes_count, disproved_count, summary }) {
|
|
975
|
+
async function reviewComplete({ story_id, review_type, findings_count, fixes_count, disproved_count, summary, claim }) {
|
|
975
976
|
const body = { review_type };
|
|
976
977
|
if (findings_count != null) body.findings_count = findings_count;
|
|
977
978
|
if (fixes_count != null) body.fixes_count = fixes_count;
|
|
978
979
|
if (disproved_count != null) body.disproved_count = disproved_count;
|
|
979
980
|
if (summary) body.summary = summary;
|
|
981
|
+
if (claim) body.claim = claim;
|
|
980
982
|
|
|
981
983
|
const result = await apiCall(
|
|
982
984
|
"POST",
|
|
@@ -989,10 +991,11 @@ async function reviewComplete({ story_id, review_type, findings_count, fixes_cou
|
|
|
989
991
|
|
|
990
992
|
// --- Verification Tools (orch key) ---
|
|
991
993
|
|
|
992
|
-
async function verifyStory({ story_id, summary, review_type }) {
|
|
994
|
+
async function verifyStory({ story_id, summary, review_type, claim }) {
|
|
993
995
|
const body = {};
|
|
994
996
|
if (summary) body.summary = summary;
|
|
995
997
|
if (review_type) body.review_type = review_type;
|
|
998
|
+
if (claim) body.claim = claim;
|
|
996
999
|
|
|
997
1000
|
const result = await apiCall(
|
|
998
1001
|
"POST",
|
|
@@ -2389,22 +2392,224 @@ async function listRoutes() {
|
|
|
2389
2392
|
return toContent(result);
|
|
2390
2393
|
}
|
|
2391
2394
|
|
|
2392
|
-
// US-26.2.3: Dispatch lineage tool
|
|
2395
|
+
// US-26.2.3: Dispatch lineage tool. LCP-1 §9.2: optionally enroll an agent key
|
|
2396
|
+
// (agent_pubkey + alg) with an owner/parent attestation over it.
|
|
2393
2397
|
async function createDispatch({
|
|
2394
2398
|
parent_dispatch_id,
|
|
2395
2399
|
role,
|
|
2396
2400
|
story_id,
|
|
2397
2401
|
agent_id,
|
|
2398
2402
|
expires_in_seconds = 3600,
|
|
2403
|
+
agent_pubkey,
|
|
2404
|
+
alg,
|
|
2405
|
+
attestation,
|
|
2406
|
+
attestation_conditions,
|
|
2399
2407
|
}) {
|
|
2400
2408
|
const body = { role, agent_id, expires_in_seconds };
|
|
2401
2409
|
if (parent_dispatch_id) body.parent_dispatch_id = parent_dispatch_id;
|
|
2402
2410
|
if (story_id) body.story_id = story_id;
|
|
2411
|
+
// LCP-1 §9.2 signed-profile enrollment (all-or-nothing).
|
|
2412
|
+
if (agent_pubkey) {
|
|
2413
|
+
body.agent_pubkey = agent_pubkey;
|
|
2414
|
+
body.alg = alg || "ed25519";
|
|
2415
|
+
if (attestation) body.attestation = attestation;
|
|
2416
|
+
if (attestation_conditions !== undefined)
|
|
2417
|
+
body.attestation_conditions = attestation_conditions;
|
|
2418
|
+
}
|
|
2403
2419
|
|
|
2404
2420
|
const result = await apiCall("POST", "/api/v1/dispatches", body);
|
|
2405
2421
|
return toContent(result);
|
|
2406
2422
|
}
|
|
2407
2423
|
|
|
2424
|
+
// LCP-1 §9.2: register/rotate the tenant custody owner key (root of trust).
|
|
2425
|
+
// ROTATION (an owner key already exists) requires `rotation_proof`: a hex Ed25519
|
|
2426
|
+
// signature by the OUTGOING owner key over owner_rotation_preimage(tenant_id,
|
|
2427
|
+
// new_pubkey, new_alg). First registration omits it. Proof of possession of the
|
|
2428
|
+
// retiring root key is what stops a stolen :user key from re-rooting trust.
|
|
2429
|
+
async function registerCustodyOwnerKey({ owner_pubkey, alg = "ed25519", rotation_proof }) {
|
|
2430
|
+
const body = { owner_pubkey, alg };
|
|
2431
|
+
if (rotation_proof) body.rotation_proof = rotation_proof;
|
|
2432
|
+
const result = await apiCall(
|
|
2433
|
+
"POST",
|
|
2434
|
+
"/api/v1/tenants/me/custody-owner-key",
|
|
2435
|
+
body,
|
|
2436
|
+
process.env.LOOPCTL_USER_KEY,
|
|
2437
|
+
);
|
|
2438
|
+
return toContent(result);
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// LCP-1 §9.1.1: transparency read of the enrolled agent-key set from the chain.
|
|
2442
|
+
async function listEnrolledAgentKeys({ limit, cursor } = {}) {
|
|
2443
|
+
const qs = new URLSearchParams();
|
|
2444
|
+
if (limit) qs.set("limit", String(limit));
|
|
2445
|
+
if (cursor) qs.set("cursor", String(cursor));
|
|
2446
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
2447
|
+
const result = await apiCall("GET", `/api/v1/dispatches/enrolled-keys${suffix}`);
|
|
2448
|
+
return toContent(result);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// --- LCP-1 §9 client-side signing helpers (Ed25519 via node crypto) ---
|
|
2452
|
+
//
|
|
2453
|
+
// The agent's private key never leaves this local process (it is the agent's own
|
|
2454
|
+
// tool). These helpers generate the keypair and produce the length-prefixed,
|
|
2455
|
+
// domain-separated preimages of LCP-1 §9.2/§9.3, then Ed25519-sign them, so an
|
|
2456
|
+
// agent can enroll and sign claims without reimplementing the wire format.
|
|
2457
|
+
|
|
2458
|
+
function lcpLp(buf) {
|
|
2459
|
+
const len = Buffer.alloc(8);
|
|
2460
|
+
len.writeBigUInt64BE(BigInt(buf.length));
|
|
2461
|
+
return Buffer.concat([len, Buffer.from(buf)]);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
function lcpPresent(strOrNull) {
|
|
2465
|
+
// Only null/undefined is ABSENT (0x00). A present-but-EMPTY string is present
|
|
2466
|
+
// (0x01 || LP("")), matching Elixir SignedProfile.present/1 exactly — the Elixir
|
|
2467
|
+
// suite asserts a nil optional and an empty-string optional produce DIFFERENT
|
|
2468
|
+
// preimages, so collapsing "" to absent here would break that invariant and make
|
|
2469
|
+
// a claim signed with capability="" fail to verify server-side.
|
|
2470
|
+
if (strOrNull === null || strOrNull === undefined) return Buffer.from([0]);
|
|
2471
|
+
return Buffer.concat([Buffer.from([1]), lcpLp(Buffer.from(strOrNull, "utf8"))]);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
function lcpCanonicalJson(value) {
|
|
2475
|
+
if (Array.isArray(value))
|
|
2476
|
+
return "[" + value.map(lcpCanonicalJson).join(",") + "]";
|
|
2477
|
+
if (value && typeof value === "object") {
|
|
2478
|
+
const keys = Object.keys(value).sort();
|
|
2479
|
+
return (
|
|
2480
|
+
"{" +
|
|
2481
|
+
keys.map((k) => JSON.stringify(k) + ":" + lcpCanonicalJson(value[k])).join(",") +
|
|
2482
|
+
"}"
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
// Elixir LeafHash.canonical_json routes EVERY number through Decimal
|
|
2486
|
+
// normalization (a single full-decimal string, never scientific notation), which
|
|
2487
|
+
// this canonicalizer does not replicate — JSON.stringify(1e22) yields "1e+22"
|
|
2488
|
+
// while Elixir yields "10000000000000000000000". v1 signs only an empty `body`
|
|
2489
|
+
// and UUID-STRING lineage paths, so numbers never appear; refuse them LOUDLY
|
|
2490
|
+
// rather than emit a signature that would silently fail to verify across the
|
|
2491
|
+
// JS/Elixir boundary once body signing (finding/artifact content) lands. Aligning
|
|
2492
|
+
// the number handling is a prerequisite for enabling body signing.
|
|
2493
|
+
if (typeof value === "number" || typeof value === "bigint") {
|
|
2494
|
+
throw new Error(
|
|
2495
|
+
"lcpCanonicalJson: numeric values are not supported yet — the JS canonicalizer " +
|
|
2496
|
+
"does not match Elixir's Decimal number normalization (LCP-1 canonical_json). " +
|
|
2497
|
+
"v1 signs an empty body; do not sign numeric fields until this is aligned.",
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
return JSON.stringify(value);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
function lcpSign(preimage, privateKeyObj) {
|
|
2504
|
+
const digest = crypto.createHash("sha256").update(preimage).digest();
|
|
2505
|
+
return crypto.sign(null, digest, privateKeyObj);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
function lcpEd25519FromRawPrivate(hex) {
|
|
2509
|
+
// Wrap a 32-byte raw Ed25519 seed as a PKCS8 key node can sign with.
|
|
2510
|
+
const seed = Buffer.from(hex, "hex");
|
|
2511
|
+
const pkcs8 = Buffer.concat([
|
|
2512
|
+
Buffer.from("302e020100300506032b657004220420", "hex"),
|
|
2513
|
+
seed,
|
|
2514
|
+
]);
|
|
2515
|
+
return crypto.createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" });
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
async function custodyGenerateKeypair() {
|
|
2519
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
2520
|
+
const rawPub = publicKey.export({ format: "der", type: "spki" }).slice(-32);
|
|
2521
|
+
const rawPriv = privateKey.export({ format: "der", type: "pkcs8" }).slice(-32);
|
|
2522
|
+
return toContent({
|
|
2523
|
+
alg: "ed25519",
|
|
2524
|
+
public_key_hex: rawPub.toString("hex"),
|
|
2525
|
+
private_key_hex: rawPriv.toString("hex"),
|
|
2526
|
+
note:
|
|
2527
|
+
"Keep private_key_hex secret and local. Register public_key_hex (as an owner key " +
|
|
2528
|
+
"or enroll it as an agent key with an attestation). Sign claims with custody_sign_claim.",
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
async function custodySignAttestation({
|
|
2533
|
+
tenant_id,
|
|
2534
|
+
agent_pubkey,
|
|
2535
|
+
lineage_path = [],
|
|
2536
|
+
conditions = "",
|
|
2537
|
+
authorizer_private_key_hex,
|
|
2538
|
+
}) {
|
|
2539
|
+
const preimage = Buffer.concat([
|
|
2540
|
+
lcpLp(Buffer.from("loopctl/dispatch-attestation/1", "utf8")),
|
|
2541
|
+
lcpLp(Buffer.from("ed25519", "utf8")),
|
|
2542
|
+
lcpLp(Buffer.from(tenant_id, "utf8")),
|
|
2543
|
+
lcpLp(Buffer.from(agent_pubkey, "hex")),
|
|
2544
|
+
lcpLp(Buffer.from(lcpCanonicalJson(lineage_path), "utf8")),
|
|
2545
|
+
lcpLp(Buffer.from(conditions, "utf8")),
|
|
2546
|
+
]);
|
|
2547
|
+
const sig = lcpSign(preimage, lcpEd25519FromRawPrivate(authorizer_private_key_hex));
|
|
2548
|
+
return toContent({ alg: "ed25519", attestation: sig.toString("hex") });
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
async function custodySignClaim({
|
|
2552
|
+
tenant_id,
|
|
2553
|
+
gate,
|
|
2554
|
+
work_item_id,
|
|
2555
|
+
capability_id,
|
|
2556
|
+
body = {},
|
|
2557
|
+
claimed_at,
|
|
2558
|
+
agent_private_key_hex,
|
|
2559
|
+
}) {
|
|
2560
|
+
const ts = claimed_at || Math.floor(Date.now() / 1000);
|
|
2561
|
+
const tsBuf = Buffer.alloc(8);
|
|
2562
|
+
tsBuf.writeBigUInt64BE(BigInt(ts));
|
|
2563
|
+
const preimage = Buffer.concat([
|
|
2564
|
+
lcpLp(Buffer.from("loopctl/custody-claim/1", "utf8")),
|
|
2565
|
+
lcpLp(Buffer.from("ed25519", "utf8")),
|
|
2566
|
+
lcpLp(Buffer.from(tenant_id, "utf8")),
|
|
2567
|
+
lcpLp(Buffer.from(gate, "utf8")),
|
|
2568
|
+
lcpPresent(work_item_id),
|
|
2569
|
+
lcpLp(Buffer.from(lcpCanonicalJson(body), "utf8")),
|
|
2570
|
+
lcpPresent(capability_id),
|
|
2571
|
+
tsBuf,
|
|
2572
|
+
]);
|
|
2573
|
+
const sig = lcpSign(preimage, lcpEd25519FromRawPrivate(agent_private_key_hex));
|
|
2574
|
+
return toContent({
|
|
2575
|
+
claim: { alg: "ed25519", claim_sig: sig.toString("hex"), claimed_at: ts },
|
|
2576
|
+
note: "Attach `claim` to the report/review-complete/verify request body under the signed profile.",
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
async function custodySignOwnerRotation({
|
|
2581
|
+
tenant_id,
|
|
2582
|
+
old_pubkey_hex,
|
|
2583
|
+
old_set_at_unix_micros,
|
|
2584
|
+
new_pubkey_hex,
|
|
2585
|
+
new_alg = "ed25519",
|
|
2586
|
+
old_private_key_hex,
|
|
2587
|
+
}) {
|
|
2588
|
+
// LCP-1 §9.2 owner-key rotation proof. Signed by the OUTGOING (retiring) owner
|
|
2589
|
+
// private key to prove possession before it re-roots the attestation chain. The
|
|
2590
|
+
// preimage has NO alg element after the domain (unlike attestation/claim), and
|
|
2591
|
+
// binds old_set_at as a raw uint64 of MICROSECONDS so a captured proof is not
|
|
2592
|
+
// replayable after a rotate-back (see SignedProfile.owner_rotation_preimage/5).
|
|
2593
|
+
const setAtBuf = Buffer.alloc(8);
|
|
2594
|
+
setAtBuf.writeBigUInt64BE(BigInt(old_set_at_unix_micros));
|
|
2595
|
+
const preimage = Buffer.concat([
|
|
2596
|
+
lcpLp(Buffer.from("loopctl/owner-key-rotation/2", "utf8")),
|
|
2597
|
+
lcpLp(Buffer.from(tenant_id, "utf8")),
|
|
2598
|
+
lcpLp(Buffer.from(old_pubkey_hex, "hex")),
|
|
2599
|
+
setAtBuf,
|
|
2600
|
+
lcpLp(Buffer.from(new_pubkey_hex, "hex")),
|
|
2601
|
+
lcpLp(Buffer.from(new_alg, "utf8")),
|
|
2602
|
+
]);
|
|
2603
|
+
const sig = lcpSign(preimage, lcpEd25519FromRawPrivate(old_private_key_hex));
|
|
2604
|
+
return toContent({
|
|
2605
|
+
rotation_proof: sig.toString("hex"),
|
|
2606
|
+
note:
|
|
2607
|
+
"Pass rotation_proof as `rotation_proof` to register_custody_owner_key (with the NEW " +
|
|
2608
|
+
"public_key). old_set_at_unix_micros is the retiring key's set-at in Unix MICROSECONDS " +
|
|
2609
|
+
"(from the tenant's custody_owner_key_set_at); a wrong unit will fail verification.",
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2408
2613
|
// US-26.7.1: public, agent-rooted (KB-tier) self-signup. No API key required —
|
|
2409
2614
|
// this creates the tenant AND the key. The resulting tenant is KB-tier only
|
|
2410
2615
|
// (knowledge ingest/search/curate on the caller's own BYO LLM keys); the
|
|
@@ -2513,6 +2718,24 @@ async function getAcceptanceCriteria({ story_id }) {
|
|
|
2513
2718
|
// Tool definitions
|
|
2514
2719
|
// ---------------------------------------------------------------------------
|
|
2515
2720
|
|
|
2721
|
+
// LCP-1 §9.3 signed-profile claim object. Attach to a report/review-complete/verify
|
|
2722
|
+
// request when the deployment runs the `signed` custody profile — produce it with
|
|
2723
|
+
// custody_sign_claim (gate must match the tool). Ignored under the default `bearer`
|
|
2724
|
+
// profile, so it is always OPTIONAL and safe to omit.
|
|
2725
|
+
const CLAIM_SCHEMA = {
|
|
2726
|
+
type: "object",
|
|
2727
|
+
description:
|
|
2728
|
+
"Optional LCP-1 §9.3 signed custody claim (produced by custody_sign_claim). Required " +
|
|
2729
|
+
"ONLY when this deployment runs the signed custody profile and your dispatch is enrolled " +
|
|
2730
|
+
"with an agent key; ignored under the default bearer profile.",
|
|
2731
|
+
properties: {
|
|
2732
|
+
alg: { type: "string", description: "Signature algorithm, e.g. \"ed25519\"." },
|
|
2733
|
+
claim_sig: { type: "string", description: "Lowercase hex signature over the §9.3 claim preimage." },
|
|
2734
|
+
claimed_at: { type: "integer", description: "Unix seconds the claim was signed (freshness-checked)." },
|
|
2735
|
+
},
|
|
2736
|
+
required: ["alg", "claim_sig", "claimed_at"],
|
|
2737
|
+
};
|
|
2738
|
+
|
|
2516
2739
|
const TOOLS = [
|
|
2517
2740
|
// Project Tools
|
|
2518
2741
|
{
|
|
@@ -3252,6 +3475,7 @@ const TOOLS = [
|
|
|
3252
3475
|
cost_millicents: { type: "integer", description: "Total cost in millicents (1/1000 of a cent)." },
|
|
3253
3476
|
},
|
|
3254
3477
|
},
|
|
3478
|
+
claim: CLAIM_SCHEMA,
|
|
3255
3479
|
},
|
|
3256
3480
|
required: ["story_id"],
|
|
3257
3481
|
},
|
|
@@ -3289,6 +3513,7 @@ const TOOLS = [
|
|
|
3289
3513
|
type: "string",
|
|
3290
3514
|
description: "Optional: summary of the review outcome.",
|
|
3291
3515
|
},
|
|
3516
|
+
claim: CLAIM_SCHEMA,
|
|
3292
3517
|
},
|
|
3293
3518
|
required: ["story_id", "review_type"],
|
|
3294
3519
|
},
|
|
@@ -3316,6 +3541,7 @@ const TOOLS = [
|
|
|
3316
3541
|
type: "string",
|
|
3317
3542
|
description: "Optional: review type for the verification record.",
|
|
3318
3543
|
},
|
|
3544
|
+
claim: CLAIM_SCHEMA,
|
|
3319
3545
|
},
|
|
3320
3546
|
required: ["story_id"],
|
|
3321
3547
|
},
|
|
@@ -5781,11 +6007,154 @@ const TOOLS = [
|
|
|
5781
6007
|
description: "Key lifetime in seconds (default 3600, max 14400).",
|
|
5782
6008
|
default: 3600,
|
|
5783
6009
|
},
|
|
6010
|
+
agent_pubkey: {
|
|
6011
|
+
type: "string",
|
|
6012
|
+
description:
|
|
6013
|
+
"LCP-1 §9.2 signed profile: hex-encoded 32-byte Ed25519 public key to enroll for " +
|
|
6014
|
+
"this dispatch. When set, an `attestation` is REQUIRED. Generate a keypair with " +
|
|
6015
|
+
"custody_generate_keypair.",
|
|
6016
|
+
},
|
|
6017
|
+
alg: {
|
|
6018
|
+
type: "string",
|
|
6019
|
+
enum: ["ed25519"],
|
|
6020
|
+
description: "Signature algorithm for the enrolled key (default ed25519).",
|
|
6021
|
+
},
|
|
6022
|
+
attestation: {
|
|
6023
|
+
type: "string",
|
|
6024
|
+
description:
|
|
6025
|
+
"LCP-1 §9.2 owner/parent attestation (hex) over agent_pubkey. Produce it with " +
|
|
6026
|
+
"custody_sign_attestation using the tenant OWNER key (root) or the PARENT dispatch's " +
|
|
6027
|
+
"agent key (delegation).",
|
|
6028
|
+
},
|
|
6029
|
+
attestation_conditions: {
|
|
6030
|
+
type: "string",
|
|
6031
|
+
description: "Optional §9.2 conditions string (e.g. gate=verify&expires<UNIX). Default empty.",
|
|
6032
|
+
},
|
|
5784
6033
|
},
|
|
5785
6034
|
required: ["role", "agent_id"],
|
|
5786
6035
|
},
|
|
5787
6036
|
},
|
|
5788
6037
|
|
|
6038
|
+
// LCP-1 §9 signed-profile tools
|
|
6039
|
+
{
|
|
6040
|
+
name: "register_custody_owner_key",
|
|
6041
|
+
description:
|
|
6042
|
+
"LCP-1 §9.2: register/rotate the tenant CUSTODY OWNER KEY — the root of trust the whole " +
|
|
6043
|
+
"attestation chain hangs from. Its private half stays with YOU (never the server); enroll " +
|
|
6044
|
+
"agent keys by signing attestations with it. Requires a user key (LOOPCTL_USER_KEY) and a " +
|
|
6045
|
+
"human-anchored tenant. Generate the keypair with custody_generate_keypair, then pass its " +
|
|
6046
|
+
"public_key_hex here. ROTATION (replacing an existing owner key) additionally requires " +
|
|
6047
|
+
"`rotation_proof` — a possession signature by the OUTGOING key, produced with " +
|
|
6048
|
+
"custody_sign_owner_rotation; first registration needs no proof.",
|
|
6049
|
+
inputSchema: {
|
|
6050
|
+
type: "object",
|
|
6051
|
+
properties: {
|
|
6052
|
+
owner_pubkey: { type: "string", description: "Hex-encoded 32-byte Ed25519 public key." },
|
|
6053
|
+
alg: { type: "string", enum: ["ed25519"], description: "Default ed25519." },
|
|
6054
|
+
rotation_proof: {
|
|
6055
|
+
type: "string",
|
|
6056
|
+
description:
|
|
6057
|
+
"Hex signature by the OUTGOING owner key authorizing the rotation (LCP-1 §9.2). " +
|
|
6058
|
+
"Required when replacing an existing owner key; produce it with custody_sign_owner_rotation.",
|
|
6059
|
+
},
|
|
6060
|
+
},
|
|
6061
|
+
required: ["owner_pubkey"],
|
|
6062
|
+
},
|
|
6063
|
+
},
|
|
6064
|
+
{
|
|
6065
|
+
name: "list_enrolled_agent_keys",
|
|
6066
|
+
description:
|
|
6067
|
+
"LCP-1 §9.1.1 transparency: list the agent public keys enrolled under your tenant, " +
|
|
6068
|
+
"reconstructed from the tamper-evident audit chain (not a mutable server listing). " +
|
|
6069
|
+
"Compare against the keys you generated; any excess is an operator-minted key. Keyset-paged.",
|
|
6070
|
+
inputSchema: {
|
|
6071
|
+
type: "object",
|
|
6072
|
+
properties: {
|
|
6073
|
+
limit: { type: "integer", description: "Max keys per page." },
|
|
6074
|
+
cursor: { type: "integer", description: "Opaque cursor from a prior page's meta.next_cursor." },
|
|
6075
|
+
},
|
|
6076
|
+
},
|
|
6077
|
+
},
|
|
6078
|
+
{
|
|
6079
|
+
name: "custody_generate_keypair",
|
|
6080
|
+
description:
|
|
6081
|
+
"LCP-1 §9: generate an Ed25519 keypair LOCALLY (the private key never leaves this process). " +
|
|
6082
|
+
"Returns public_key_hex (to register/enroll) and private_key_hex (keep secret; pass to " +
|
|
6083
|
+
"custody_sign_claim / custody_sign_attestation).",
|
|
6084
|
+
inputSchema: { type: "object", properties: {} },
|
|
6085
|
+
},
|
|
6086
|
+
{
|
|
6087
|
+
name: "custody_sign_attestation",
|
|
6088
|
+
description:
|
|
6089
|
+
"LCP-1 §9.2: sign an attestation over an agent public key, to enroll it. Sign with the " +
|
|
6090
|
+
"tenant OWNER private key for a root enrollment (lineage_path []), or the PARENT dispatch's " +
|
|
6091
|
+
"agent private key for a child (lineage_path = the parent's lineage_path). Returns the hex " +
|
|
6092
|
+
"attestation to pass to `dispatch`.",
|
|
6093
|
+
inputSchema: {
|
|
6094
|
+
type: "object",
|
|
6095
|
+
properties: {
|
|
6096
|
+
tenant_id: { type: "string", description: "Your tenant UUID." },
|
|
6097
|
+
agent_pubkey: { type: "string", description: "Hex agent public key being enrolled." },
|
|
6098
|
+
lineage_path: {
|
|
6099
|
+
type: "array",
|
|
6100
|
+
items: { type: "string" },
|
|
6101
|
+
description: "Authorizer's lineage: [] for owner-root, the parent's lineage_path for a child.",
|
|
6102
|
+
},
|
|
6103
|
+
conditions: { type: "string", description: "Optional conditions string." },
|
|
6104
|
+
authorizer_private_key_hex: {
|
|
6105
|
+
type: "string",
|
|
6106
|
+
description: "Hex private key of the owner (root) or parent agent (child).",
|
|
6107
|
+
},
|
|
6108
|
+
},
|
|
6109
|
+
required: ["tenant_id", "agent_pubkey", "authorizer_private_key_hex"],
|
|
6110
|
+
},
|
|
6111
|
+
},
|
|
6112
|
+
{
|
|
6113
|
+
name: "custody_sign_claim",
|
|
6114
|
+
description:
|
|
6115
|
+
"LCP-1 §9.3: sign a custody claim with your enrolled agent private key. Returns a `claim` " +
|
|
6116
|
+
"object to attach to the report/review-complete/verify request body when the deployment " +
|
|
6117
|
+
"runs the signed profile. Binds gate + work_item_id + capability + claimed_at.",
|
|
6118
|
+
inputSchema: {
|
|
6119
|
+
type: "object",
|
|
6120
|
+
properties: {
|
|
6121
|
+
tenant_id: { type: "string", description: "Your tenant UUID." },
|
|
6122
|
+
gate: { type: "string", enum: ["report", "review_complete", "verify"] },
|
|
6123
|
+
work_item_id: { type: "string", description: "The story UUID." },
|
|
6124
|
+
capability_id: { type: "string", description: "Optional capability token id." },
|
|
6125
|
+
claimed_at: { type: "integer", description: "Unix seconds (default: now)." },
|
|
6126
|
+
agent_private_key_hex: { type: "string", description: "Hex agent private key." },
|
|
6127
|
+
},
|
|
6128
|
+
required: ["tenant_id", "gate", "work_item_id", "agent_private_key_hex"],
|
|
6129
|
+
},
|
|
6130
|
+
},
|
|
6131
|
+
{
|
|
6132
|
+
name: "custody_sign_owner_rotation",
|
|
6133
|
+
description:
|
|
6134
|
+
"LCP-1 §9.2: sign an owner-key ROTATION proof with the OUTGOING (retiring) owner private " +
|
|
6135
|
+
"key, proving possession before it re-roots the attestation chain. Returns `rotation_proof` " +
|
|
6136
|
+
"to pass to register_custody_owner_key alongside the NEW public key. Binds the old key + its " +
|
|
6137
|
+
"set-at (Unix MICROSECONDS) so the proof is not replayable after a rotate-back. First " +
|
|
6138
|
+
"registration needs no proof — use this only to REPLACE an existing owner key.",
|
|
6139
|
+
inputSchema: {
|
|
6140
|
+
type: "object",
|
|
6141
|
+
properties: {
|
|
6142
|
+
tenant_id: { type: "string", description: "Your tenant UUID." },
|
|
6143
|
+
old_pubkey_hex: { type: "string", description: "Hex public key of the OUTGOING owner key." },
|
|
6144
|
+
old_set_at_unix_micros: {
|
|
6145
|
+
type: "integer",
|
|
6146
|
+
description:
|
|
6147
|
+
"The outgoing key's set-at in Unix MICROSECONDS (the tenant's custody_owner_key_set_at). " +
|
|
6148
|
+
"A wrong unit (e.g. seconds/millis) will fail server-side verification.",
|
|
6149
|
+
},
|
|
6150
|
+
new_pubkey_hex: { type: "string", description: "Hex public key of the NEW owner key." },
|
|
6151
|
+
new_alg: { type: "string", enum: ["ed25519"], description: "New key algorithm (default ed25519)." },
|
|
6152
|
+
old_private_key_hex: { type: "string", description: "Hex private key of the OUTGOING owner key." },
|
|
6153
|
+
},
|
|
6154
|
+
required: ["tenant_id", "old_pubkey_hex", "old_set_at_unix_micros", "new_pubkey_hex", "old_private_key_hex"],
|
|
6155
|
+
},
|
|
6156
|
+
},
|
|
6157
|
+
|
|
5789
6158
|
// Chain of Custody v2 tools
|
|
5790
6159
|
{
|
|
5791
6160
|
name: "signup",
|
|
@@ -6364,6 +6733,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6364
6733
|
case "dispatch":
|
|
6365
6734
|
return await createDispatch(args);
|
|
6366
6735
|
|
|
6736
|
+
case "register_custody_owner_key":
|
|
6737
|
+
return await registerCustodyOwnerKey(args);
|
|
6738
|
+
|
|
6739
|
+
case "list_enrolled_agent_keys":
|
|
6740
|
+
return await listEnrolledAgentKeys(args);
|
|
6741
|
+
|
|
6742
|
+
case "custody_generate_keypair":
|
|
6743
|
+
return await custodyGenerateKeypair(args);
|
|
6744
|
+
|
|
6745
|
+
case "custody_sign_attestation":
|
|
6746
|
+
return await custodySignAttestation(args);
|
|
6747
|
+
|
|
6748
|
+
case "custody_sign_claim":
|
|
6749
|
+
return await custodySignClaim(args);
|
|
6750
|
+
|
|
6751
|
+
case "custody_sign_owner_rotation":
|
|
6752
|
+
return await custodySignOwnerRotation(args);
|
|
6753
|
+
|
|
6367
6754
|
case "signup":
|
|
6368
6755
|
return await signup(args);
|
|
6369
6756
|
|