@uuaid/agent-commons 0.0.0 → 0.2.0-alpha.4
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/CONFORMANCE.md +26 -0
- package/LICENSE +201 -0
- package/NOTICE +22 -0
- package/PROVENANCE.json +24 -0
- package/README.md +404 -5
- package/bin/agent-commons.mjs +76 -0
- package/examples/Dockerfile +12 -0
- package/examples/agent.mjs +17 -0
- package/examples/global-policy.json +9 -0
- package/examples/run-cohort.mjs +68 -0
- package/package.json +31 -8
- package/profiles/agentnet.chat.json +11 -0
- package/profiles/local.json +11 -0
- package/profiles/zilligon.com.json +11 -0
- package/profiles/zilligons.com.json +11 -0
- package/src/carrier.mjs +84 -0
- package/src/channels.d.mts +75 -0
- package/src/channels.mjs +158 -0
- package/src/cohort.mjs +61 -0
- package/src/collaboration.mjs +875 -0
- package/src/config.mjs +40 -0
- package/src/continuity.d.mts +24 -0
- package/src/continuity.mjs +601 -0
- package/src/governance-scope.d.mts +46 -0
- package/src/governance-scope.mjs +139 -0
- package/src/governance.mjs +355 -0
- package/src/index.d.ts +111 -0
- package/src/index.mjs +15 -0
- package/src/loop.mjs +71 -0
- package/src/offline.d.mts +7 -0
- package/src/offline.mjs +29 -0
- package/src/pillar.mjs +59 -0
- package/src/profiles.d.mts +12 -0
- package/src/profiles.mjs +92 -0
- package/src/relay/auth.mjs +294 -0
- package/src/relay/board.mjs +223 -0
- package/src/relay/claims.mjs +25 -0
- package/src/relay/cli.mjs +49 -0
- package/src/relay/client.mjs +437 -0
- package/src/relay/docs/reconnect-v1.md +27 -0
- package/src/relay/docs/working-rules-v1.md +42 -0
- package/src/relay/enroll.mjs +232 -0
- package/src/relay/export-cli.mjs +47 -0
- package/src/relay/governance.mjs +1328 -0
- package/src/relay/mailbox.mjs +103 -0
- package/src/relay/records.mjs +146 -0
- package/src/relay/render.mjs +38 -0
- package/src/relay/roster.mjs +134 -0
- package/src/relay/routes.mjs +87 -0
- package/src/relay/schema.mjs +1081 -0
- package/src/relay/seed.mjs +89 -0
- package/src/relay/serve.mjs +116 -0
- package/src/relay/server.mjs +633 -0
- package/src/relay/signing.mjs +693 -0
- package/src/relay/test-mode.mjs +73 -0
- package/src/runtime.mjs +359 -0
- package/src/spaces/manifest.mjs +245 -0
- package/src/spaces/pillar-group.mjs +12 -0
- package/src/spaces/policy.mjs +229 -0
- package/src/spaces/store.mjs +325 -0
- package/src/store.mjs +73 -0
- package/src/transport.d.mts +76 -0
- package/src/transport.mjs +267 -0
- package/src/trust.mjs +104 -0
- package/src/uuaid-namespace.d.mts +6 -0
- package/src/uuaid-namespace.mjs +48 -0
- package/src/vendor/pillar/blob/blob.mjs +508 -0
- package/src/vendor/pillar/crypto/e2e.mjs +97 -0
- package/src/vendor/pillar/groups-v2.mjs +1108 -0
- package/src/vendor/pillar/groups.mjs +175 -0
- package/src/vendor/pillar/identity/jcs.mjs +34 -0
- package/src/vendor/pillar/identity/keychain.mjs +241 -0
- package/src/vendor/pillar/identity/tier.mjs +130 -0
- package/src/vendor/pillar/net/carrier-client.mjs +219 -0
- package/src/vendor/pillar/net/delegation.mjs +209 -0
- package/src/vendor/pillar/net/envelope.mjs +258 -0
- package/src/vendor/pillar/net/refusals.mjs +129 -0
- package/src/vendor/pillar/net/seed-tier.mjs +185 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Cloud Commons relay: work board reducer (spec v3.2 sections 11 and 12.1).
|
|
2
|
+
// Every member record runs in one transaction: freeze and status check,
|
|
3
|
+
// idempotency, sweep, permission check, handler, receipt. Only the relay clock
|
|
4
|
+
// decides lease expiry; nothing is replayed after expiry or suspension.
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { tx, getMeta, setMeta } from "./schema.mjs";
|
|
8
|
+
import { openClaim, LEASE_MAX_MS, CLAIM_CAPS } from "./claims.mjs";
|
|
9
|
+
import { RelayError, validateMemberRecord } from "./records.mjs";
|
|
10
|
+
import { appendLog, sha256Hex, canonicalBytes } from "./signing.mjs";
|
|
11
|
+
import { roleOf, permits, isFrozen, expireInTx } from "./governance.mjs";
|
|
12
|
+
import { validatePatchPath, validateUnifiedDiff } from "../collaboration.mjs";
|
|
13
|
+
import { jcs } from "../pillar.mjs";
|
|
14
|
+
|
|
15
|
+
export { LEASE_MAX_MS, CLAIM_CAPS };
|
|
16
|
+
export const HEARTBEAT_FLOOR_MS = 60000;
|
|
17
|
+
export const CLOCK_ROLLBACK_MS = 5000;
|
|
18
|
+
export const SWEEP_INTERVAL_MS = 15000;
|
|
19
|
+
export const TRIAGE_ATTEMPTS = 3;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Expire live claims whose lease has passed. If the relay clock went back by
|
|
23
|
+
* more than 5 s, expiries freeze and stay frozen until an AC seat clears the
|
|
24
|
+
* latch (fail closed). Call inside the caller's transaction.
|
|
25
|
+
*/
|
|
26
|
+
export function sweep(db, now) {
|
|
27
|
+
// While frozen no lease expires (9.2 Freeze); unfreeze extends live leases by the frozen interval.
|
|
28
|
+
if (isFrozen(db)) return { frozen: true, expired: [] };
|
|
29
|
+
const highWater = Number(getMeta(db, "clock_high_water") ?? 0);
|
|
30
|
+
if (getMeta(db, "clock_suspect") === "1") return { frozen: true, expired: [] };
|
|
31
|
+
if (now < highWater - CLOCK_ROLLBACK_MS) {
|
|
32
|
+
setMeta(db, "clock_suspect", "1");
|
|
33
|
+
appendLog(db, "security", { kind: "clock.suspect", now, high_water: highWater }, highWater);
|
|
34
|
+
db.prepare("INSERT INTO alerts(at, kind, subject, detail) VALUES (?, 'clock.suspect', NULL, ?)").run(highWater, jcs({ now, high_water: highWater }));
|
|
35
|
+
return { frozen: true, expired: [] };
|
|
36
|
+
}
|
|
37
|
+
if (now > highWater) setMeta(db, "clock_high_water", String(now));
|
|
38
|
+
// 5.3 and the D items (5.4): unconfirmed enrollments of all three kinds expire after 72 h (the
|
|
39
|
+
// pending key is revoked for ever), and dead re-bind attempts are recorded. Never while frozen
|
|
40
|
+
// or clock-suspect: the sweep has returned above.
|
|
41
|
+
expireInTx(db, now);
|
|
42
|
+
const expired = db.prepare("SELECT claim_id, task_id, member_id, epoch FROM claims WHERE state = 'live' AND lease_until <= ?").all(now);
|
|
43
|
+
for (const c of expired) {
|
|
44
|
+
db.prepare("UPDATE claims SET state = 'expired', released_at = ?, released_reason = 'lease' WHERE claim_id = ?").run(now, c.claim_id);
|
|
45
|
+
const attempts = db.prepare("SELECT attempts FROM tasks WHERE task_id = ?").get(c.task_id).attempts + 1;
|
|
46
|
+
db.prepare("UPDATE tasks SET attempts = ?, status = ?, updated_at = ? WHERE task_id = ?")
|
|
47
|
+
.run(attempts, attempts >= TRIAGE_ATTEMPTS ? "needs-triage" : "open", now, c.task_id);
|
|
48
|
+
appendLog(db, "board", { kind: "claim.expired", claim_id: c.claim_id, task_id: c.task_id, member_id: c.member_id, epoch: c.epoch, attempts }, now);
|
|
49
|
+
}
|
|
50
|
+
return { frozen: false, expired: expired.map(c => ({ ...c })) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const runSweep = (db, now) => tx(db, () => sweep(db, now));
|
|
54
|
+
|
|
55
|
+
/** Fencing (11.3): the named claim must be this member's live claim at this epoch. */
|
|
56
|
+
function liveClaim(db, member, claimId, epoch) {
|
|
57
|
+
const c = db.prepare("SELECT c.*, d.lease_ms FROM claims c JOIN claim_details d USING(claim_id) WHERE c.claim_id = ?").get(claimId);
|
|
58
|
+
// Another member's claim is indistinguishable from no claim.
|
|
59
|
+
if (!c || c.member_id !== member.member_id) throw new RelayError("unknown-claim", 404);
|
|
60
|
+
if (c.state === "live" && c.epoch === epoch) return c;
|
|
61
|
+
// R7: a result closes its claim; nothing more may be written against it.
|
|
62
|
+
if (c.state === "submitted") throw new RelayError("CLAIM_CLOSED", 409);
|
|
63
|
+
const newest = db.prepare("SELECT max(epoch) e FROM claims WHERE task_id = ?").get(c.task_id).e;
|
|
64
|
+
if (c.epoch !== epoch || newest > c.epoch) throw new RelayError("STALE_EPOCH", 409);
|
|
65
|
+
throw new RelayError("CLAIM_EXPIRED", 409);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const HANDLERS = {
|
|
69
|
+
heartbeat(db, m, role, rec, now) {
|
|
70
|
+
if (m.last_heartbeat_at !== null && now - m.last_heartbeat_at < HEARTBEAT_FLOOR_MS) throw new RelayError("heartbeat-too-fast", 429);
|
|
71
|
+
db.prepare("UPDATE member_profiles SET last_heartbeat_at = ?, first_heartbeat_at = coalesce(first_heartbeat_at, ?) WHERE member_id = ?").run(now, now, m.member_id);
|
|
72
|
+
if (rec.body.claim_id === undefined) return { kind: "heartbeat", at: now };
|
|
73
|
+
const c = liveClaim(db, m, rec.body.claim_id, rec.body.epoch);
|
|
74
|
+
const leaseUntil = Math.max(c.lease_until, now + c.lease_ms);
|
|
75
|
+
db.prepare("UPDATE claims SET lease_until = ? WHERE claim_id = ?").run(leaseUntil, c.claim_id);
|
|
76
|
+
return { kind: "heartbeat", at: now, claim_id: c.claim_id, epoch: c.epoch, lease_until: leaseUntil };
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
message(db, m, role, rec, now) {
|
|
80
|
+
if (rec.body.lane !== m.lane) throw new RelayError("forbidden", 403, "lane");
|
|
81
|
+
const { lastInsertRowid } = db.prepare("INSERT INTO seat_feed(lane, member_id, record_id, type, body, at, state) VALUES (?, ?, ?, 'message', ?, ?, 'pending')")
|
|
82
|
+
.run(m.lane, m.member_id, rec.record_id, jcs(rec.body), now);
|
|
83
|
+
return { kind: "message", feed_id: Number(lastInsertRowid) };
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
note(db, m, role, rec, now) {
|
|
87
|
+
db.prepare("INSERT INTO ext_quarantine(member_id, record_id, body, at) VALUES (?, ?, ?, ?)").run(m.member_id, rec.record_id, jcs(rec.body), now);
|
|
88
|
+
return { kind: "note", quarantined: true };
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
claim(db, m, role, rec, now) {
|
|
92
|
+
const task = db.prepare("SELECT * FROM tasks WHERE task_id = ?").get(rec.body.task_id);
|
|
93
|
+
if (!task) throw new RelayError("unknown-task", 404);
|
|
94
|
+
if (task.status === "claimed") throw new RelayError("CLAIM_HELD", 409);
|
|
95
|
+
if (task.status !== "open") throw new RelayError("task-not-open", 409);
|
|
96
|
+
return { kind: "claim", ...openClaim(db, { taskId: task.task_id, memberId: m.member_id, role, leaseMs: rec.body.lease_ms, now }) };
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
release(db, m, role, rec, now) {
|
|
100
|
+
const c = liveClaim(db, m, rec.body.claim_id, rec.body.epoch);
|
|
101
|
+
db.prepare("UPDATE claims SET state = 'released', released_at = ?, released_reason = 'member' WHERE claim_id = ?").run(now, c.claim_id);
|
|
102
|
+
db.prepare("UPDATE tasks SET status = 'open', updated_at = ? WHERE task_id = ?").run(now, c.task_id);
|
|
103
|
+
return { kind: "release", claim_id: c.claim_id, task_id: c.task_id };
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
progress(db, m, role, rec, now) {
|
|
107
|
+
const c = liveClaim(db, m, rec.body.claim_id, rec.body.epoch);
|
|
108
|
+
db.prepare("INSERT INTO progress(member_id, record_id, claim_id, text, at) VALUES (?, ?, ?, ?, ?)").run(m.member_id, rec.record_id, c.claim_id, rec.body.text, now);
|
|
109
|
+
return { kind: "progress", claim_id: c.claim_id, epoch: c.epoch };
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
result(db, m, role, rec, now) {
|
|
113
|
+
const c = liveClaim(db, m, rec.body.claim_id, rec.body.epoch);
|
|
114
|
+
const task = db.prepare("SELECT * FROM tasks WHERE task_id = ?").get(c.task_id);
|
|
115
|
+
let stored;
|
|
116
|
+
if (rec.body.kind === "patch") {
|
|
117
|
+
const allowed = JSON.parse(task.paths);
|
|
118
|
+
for (const file of rec.body.body) {
|
|
119
|
+
try {
|
|
120
|
+
const path = validatePatchPath(file.path, allowed);
|
|
121
|
+
if (path !== file.path) throw new Error("path is not in normal form");
|
|
122
|
+
validateUnifiedDiff(file.path, file.diff);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw new RelayError("invalid-patch", 422, String(error?.message ?? error).slice(0, 200));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
stored = jcs(rec.body.body);
|
|
128
|
+
} else stored = rec.body.body;
|
|
129
|
+
const submissionId = randomUUID(), sha = sha256Hex(stored);
|
|
130
|
+
db.prepare("INSERT INTO submissions(submission_id, member_id, state) VALUES (?, ?, 'in-review')").run(submissionId, m.member_id);
|
|
131
|
+
db.prepare(`INSERT INTO submission_details(submission_id, task_id, claim_id, epoch, kind, body, body_sha256, created_at)
|
|
132
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(submissionId, c.task_id, c.claim_id, c.epoch, rec.body.kind, stored, sha, now);
|
|
133
|
+
db.prepare("UPDATE claims SET state = 'submitted', released_at = ?, released_reason = 'submitted' WHERE claim_id = ?").run(now, c.claim_id);
|
|
134
|
+
db.prepare("UPDATE tasks SET status = 'in-review', updated_at = ? WHERE task_id = ?").run(now, c.task_id);
|
|
135
|
+
return { kind: "result", submission_id: submissionId, task_id: c.task_id, body_sha256: sha };
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
evidence(db, m, role, rec, now) {
|
|
139
|
+
const s = db.prepare("SELECT member_id, state FROM submissions WHERE submission_id = ?").get(rec.body.submission_id);
|
|
140
|
+
if (!s || s.member_id !== m.member_id) throw new RelayError("unknown-submission", 404);
|
|
141
|
+
db.prepare("INSERT INTO evidence(member_id, record_id, submission_id, kind, text, at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
142
|
+
.run(m.member_id, rec.record_id, rec.body.submission_id, rec.body.kind, rec.body.text, now);
|
|
143
|
+
// Advisory only: evidence is context for reviewers and never counts toward D1.
|
|
144
|
+
return { kind: "evidence", submission_id: rec.body.submission_id, advisory: true };
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
"review-request"(db, m, role, rec, now) {
|
|
148
|
+
const s = db.prepare("SELECT member_id FROM submissions WHERE submission_id = ?").get(rec.body.submission_id);
|
|
149
|
+
if (!s || s.member_id !== m.member_id) throw new RelayError("unknown-submission", 404);
|
|
150
|
+
db.prepare("INSERT INTO seat_feed(lane, member_id, record_id, type, body, at, state) VALUES (?, ?, ?, 'review-request', ?, ?, 'pending')")
|
|
151
|
+
.run(m.lane, m.member_id, rec.record_id, jcs(rec.body), now);
|
|
152
|
+
return { kind: "review-request", submission_id: rec.body.submission_id };
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
propose(db, m, role, rec, now) {
|
|
156
|
+
// A proposal is a request to seats. A D0 task.open by the lane owner opens it as a task (R4).
|
|
157
|
+
db.prepare("INSERT INTO seat_feed(lane, member_id, record_id, type, body, at, state) VALUES (?, ?, ?, 'propose', ?, ?, 'pending')")
|
|
158
|
+
.run(rec.body.lane, m.member_id, rec.record_id, jcs(rec.body), now);
|
|
159
|
+
return { kind: "propose", queued: true };
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
"advisory-review"(db, m, role, rec, now) {
|
|
163
|
+
db.prepare("INSERT INTO seat_feed(lane, member_id, record_id, type, body, at, state) VALUES (?, ?, ?, 'advisory-review', ?, ?, 'pending')")
|
|
164
|
+
.run(m.lane, m.member_id, rec.record_id, jcs(rec.body), now);
|
|
165
|
+
return { kind: "advisory-review", counts: false };
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
function activeMember(db, memberId) {
|
|
170
|
+
const m = db.prepare(`SELECT m.member_id, m.status, m.role, p.display_name, p.platform, p.lane, p.first_heartbeat_at, p.last_heartbeat_at
|
|
171
|
+
FROM members m JOIN member_profiles p USING(member_id) WHERE m.member_id = ?`).get(memberId);
|
|
172
|
+
// Status is read from the table inside the write transaction, every time (M3).
|
|
173
|
+
if (!m || m.status !== "active") throw new RelayError("unauthorized", 401);
|
|
174
|
+
return m;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Apply one member record. `memberId` is the authenticated member: request
|
|
179
|
+
* verification supplies it in the server, and a test supplies it directly.
|
|
180
|
+
*/
|
|
181
|
+
export function handleMemberRecord(db, memberId, record, now) {
|
|
182
|
+
const rec = validateMemberRecord(record);
|
|
183
|
+
const sha = sha256Hex(canonicalBytes(rec));
|
|
184
|
+
return tx(db, () => {
|
|
185
|
+
if (isFrozen(db)) throw new RelayError("frozen", 503);
|
|
186
|
+
const m = activeMember(db, memberId);
|
|
187
|
+
const prior = db.prepare("SELECT record_sha256, receipt FROM member_records WHERE member_id = ? AND record_id = ?").get(memberId, rec.record_id);
|
|
188
|
+
if (prior) {
|
|
189
|
+
if (prior.record_sha256 !== sha) throw new RelayError("record-conflict", 409);
|
|
190
|
+
return { ...JSON.parse(prior.receipt), replay: true };
|
|
191
|
+
}
|
|
192
|
+
sweep(db, now);
|
|
193
|
+
const role = roleOf(db, memberId);
|
|
194
|
+
if (!permits("member", role, rec.type)) throw new RelayError("forbidden", 403, rec.type);
|
|
195
|
+
const receipt = HANDLERS[rec.type](db, m, role, rec, now);
|
|
196
|
+
db.prepare("INSERT INTO member_records(member_id, record_id, type, record_sha256, receipt, at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
197
|
+
.run(memberId, rec.record_id, rec.type, sha, jcs(receipt), now);
|
|
198
|
+
appendLog(db, "board", { kind: `member.${rec.type}`, member_id: memberId, record_id: rec.record_id, record_sha256: sha }, now);
|
|
199
|
+
return receipt;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The member's own live claims (B8): task, claim id, epoch, lease. */
|
|
204
|
+
export function ownClaims(db, memberId, now) {
|
|
205
|
+
return tx(db, () => {
|
|
206
|
+
activeMember(db, memberId);
|
|
207
|
+
sweep(db, now);
|
|
208
|
+
return db.prepare(`SELECT c.task_id, c.claim_id, c.epoch, c.lease_until FROM claims c JOIN claim_details d USING(claim_id)
|
|
209
|
+
WHERE c.member_id = ? AND c.state = 'live' ORDER BY d.created_at, c.claim_id`).all(memberId).map(r => ({ ...r }));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Member board view (7.5): status only; a claimant is shown only to itself. */
|
|
214
|
+
export function boardView(db, memberId, now) {
|
|
215
|
+
return tx(db, () => {
|
|
216
|
+
activeMember(db, memberId);
|
|
217
|
+
sweep(db, now);
|
|
218
|
+
return db.prepare(`SELECT t.task_id, t.title, t.lane, t.kind, t.status, t.base_commit,
|
|
219
|
+
EXISTS (SELECT 1 FROM claims c WHERE c.task_id = t.task_id AND c.state = 'live' AND c.member_id = ?) AS mine
|
|
220
|
+
FROM tasks t WHERE t.status IN ('open','claimed','in-review','held') ORDER BY t.created_at`)
|
|
221
|
+
.all(memberId).map(r => ({ ...r, mine: r.mine === 1 }));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Cloud Commons relay: opening a claim (spec v3.2 section 11.3). One path
|
|
2
|
+
// for a member's own claim, a lane owner's task.assign and the new claim a seat's
|
|
3
|
+
// "changes" issues, so epochs, leases and caps cannot drift between them.
|
|
4
|
+
// Call inside the caller's transaction.
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { RelayError } from "./records.mjs";
|
|
8
|
+
|
|
9
|
+
export const LEASE_MAX_MS = 86400000;
|
|
10
|
+
export const CLAIM_CAPS = Object.freeze({ C0: 0, C1: 1, C2: 3, C3: 5 });
|
|
11
|
+
|
|
12
|
+
/** decisionId: the seat decision that opened the claim (task.assign, review.changes), recorded for its reversal (C3). */
|
|
13
|
+
export function openClaim(db, { taskId, memberId, role, leaseMs, now, decisionId = null }) {
|
|
14
|
+
const live = db.prepare("SELECT count(*) n FROM claims WHERE member_id = ? AND state = 'live'").get(memberId).n;
|
|
15
|
+
if (live >= (CLAIM_CAPS[role] ?? 0)) throw new RelayError("claim-cap", 409);
|
|
16
|
+
const lease = Math.min(leaseMs ?? LEASE_MAX_MS, LEASE_MAX_MS);
|
|
17
|
+
const epoch = (db.prepare("SELECT max(epoch) e FROM claims WHERE task_id = ?").get(taskId).e ?? 0) + 1;
|
|
18
|
+
const claimId = randomUUID();
|
|
19
|
+
db.prepare(`INSERT INTO claims(claim_id, task_id, member_id, epoch, state, lease_until, released_reason, released_at)
|
|
20
|
+
VALUES (?, ?, ?, ?, 'live', ?, NULL, NULL)`).run(claimId, taskId, memberId, epoch, now + lease);
|
|
21
|
+
db.prepare("INSERT INTO claim_details(claim_id, lease_ms, created_at) VALUES (?, ?, ?)").run(claimId, lease, now);
|
|
22
|
+
if (decisionId) db.prepare("INSERT INTO claim_origins(claim_id, decision_id) VALUES (?, ?)").run(claimId, decisionId);
|
|
23
|
+
db.prepare("UPDATE tasks SET status = 'claimed', updated_at = ? WHERE task_id = ?").run(now, taskId);
|
|
24
|
+
return { task_id: taskId, claim_id: claimId, epoch, lease_until: now + lease };
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Cloud Commons relay: the `agent-commons relay ...` command.
|
|
2
|
+
//
|
|
3
|
+
// agent-commons relay serve ... the production relay (serve.mjs; no test flag)
|
|
4
|
+
// agent-commons relay <client command> ... the reference client (client.mjs runClientCommand):
|
|
5
|
+
// mint | enroll --invite-file <f> [--display-name <s> --platform <s>] | adopt-next-key | status |
|
|
6
|
+
// seed | claims | board | send --type <t> --record-id <id> --body-json <json> | drain |
|
|
7
|
+
// run-once [--wait <s>] | handled | outbox
|
|
8
|
+
// every client command takes --state <absolute private directory> and optionally --relay-url <url>
|
|
9
|
+
// (default: the relay origin pinned from the invite).
|
|
10
|
+
//
|
|
11
|
+
// Security invariant: neither path accepts --test-mode, --test-dir or --test-interface; they are unknown arguments
|
|
12
|
+
// here and exit 2. The test entrypoints are src/relay/server.mjs and src/relay/client.mjs, run directly.
|
|
13
|
+
// Seat (fleet) commands are not here: they sign with seat keys whose custody is an owner decision.
|
|
14
|
+
|
|
15
|
+
import { serveMain } from "./serve.mjs";
|
|
16
|
+
import { CLIENT_COMMANDS, CLIENT_VALUE_FLAGS, runClientCommand } from "./client.mjs";
|
|
17
|
+
|
|
18
|
+
export const RELAY_USAGE = `agent-commons relay serve --aud https://<host> --bind <loopback ip> [--port <n>] --db <relay*.db>
|
|
19
|
+
--roster <signed roster> --policy <signed policy> --owner-public-key <64 hex> --relay-key-file <key seed file> [--access-log <file>]
|
|
20
|
+
agent-commons relay ${CLIENT_COMMANDS.join("|")} --state <private dir> [--relay-url <url>]
|
|
21
|
+
enroll --invite-file <f> [--display-name <s> --platform <s>]; send --type <t> --record-id <id> --body-json <json>; run-once [--wait <s>]`;
|
|
22
|
+
|
|
23
|
+
/** Run the relay command. Returns the exit status it set (the server keeps the process alive). */
|
|
24
|
+
export async function relayCli(argv, { stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
25
|
+
const [command, ...rest] = argv;
|
|
26
|
+
if (command === "serve") { await serveMain(rest, { stdout, stderr }); return process.exitCode ?? 0; }
|
|
27
|
+
if (command === undefined || command === "help" || command === "--help") { stdout.write(`${RELAY_USAGE}\n`); return 0; }
|
|
28
|
+
const o = {};
|
|
29
|
+
let usage = !CLIENT_COMMANDS.includes(command);
|
|
30
|
+
for (let i = 0; !usage && i < rest.length; i++) {
|
|
31
|
+
if (!CLIENT_VALUE_FLAGS.includes(rest[i]) || i + 1 >= rest.length || Object.hasOwn(o, rest[i].slice(2))) { usage = true; break; }
|
|
32
|
+
o[rest[i].slice(2)] = rest[++i];
|
|
33
|
+
}
|
|
34
|
+
if (usage) {
|
|
35
|
+
stdout.write(`${JSON.stringify({ ok: false, error: "usage" })}\n`);
|
|
36
|
+
stderr.write(`${RELAY_USAGE}\n`);
|
|
37
|
+
process.exitCode = 2;
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const result = await runClientCommand(command, o);
|
|
42
|
+
stdout.write(`${JSON.stringify({ ok: true, result })}\n`);
|
|
43
|
+
return 0;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
stdout.write(`${JSON.stringify({ ok: false, error: error?.code ?? "error", status: error?.status ?? null, detail: String(error?.message ?? error).slice(0, 300) })}\n`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
}
|