@mean-weasel/lineage 0.1.3 → 0.1.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/dist/server.js CHANGED
@@ -57,7 +57,7 @@ function walkLocalReviewFiles(dir, files = []) {
57
57
  throw error;
58
58
  }
59
59
  for (const entry of entries) {
60
- if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "studio-uploads") continue;
60
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "studio-uploads" || entry.name === "playwright-results" || entry.name === "lineage-demo") continue;
61
61
  if (process.env.NODE_ENV !== "test" && /^vitest-/.test(entry.name)) continue;
62
62
  const path = join(dir, entry.name);
63
63
  if (entry.isDirectory()) walkLocalReviewFiles(path, files);
@@ -595,6 +595,41 @@ function lineageDb() {
595
595
  );
596
596
  create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
597
597
  create table if not exists adapter_settings (project_id text not null references projects(id), adapter_type text not null check (adapter_type in ('cloud', 'scheduler', 'image_generator')), provider text not null, enabled integer not null check (enabled in (0, 1)), secret_ref text, safe_config_json text not null, created_at text not null, updated_at text not null, primary key(project_id, adapter_type, provider)); create index if not exists adapter_settings_project_type on adapter_settings(project_id, adapter_type);
598
+ create table if not exists agent_claims (
599
+ id text primary key,
600
+ token_hash text not null,
601
+ project_id text not null references projects(id),
602
+ channel text,
603
+ scope_type text not null check (scope_type in ('lineage_workspace', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
604
+ target_id text not null,
605
+ target_title text,
606
+ agent_id text,
607
+ agent_name text not null,
608
+ agent_kind text not null,
609
+ thread_id text,
610
+ status text not null check (status in ('active', 'expired', 'released', 'revoked', 'transferred')),
611
+ created_at text not null,
612
+ heartbeat_at text not null,
613
+ expires_at text not null,
614
+ released_at text,
615
+ revoked_at text,
616
+ revoked_by text,
617
+ override_reason text,
618
+ metadata_json text
619
+ );
620
+ create unique index if not exists agent_claims_token_hash on agent_claims(token_hash);
621
+ create index if not exists agent_claims_project_status on agent_claims(project_id, status, heartbeat_at);
622
+ create index if not exists agent_claims_target on agent_claims(project_id, channel, scope_type, target_id, status);
623
+ create table if not exists agent_claim_events (
624
+ id text primary key,
625
+ claim_id text not null references agent_claims(id) on delete cascade,
626
+ event_type text not null,
627
+ actor text,
628
+ message text,
629
+ created_at text not null,
630
+ metadata_json text
631
+ );
632
+ create index if not exists agent_claim_events_claim_created on agent_claim_events(claim_id, created_at);
598
633
  `);
599
634
  migrateAssetSelections(database);
600
635
  dropLegacyAssetSelectionRootUnique(database);
@@ -1930,6 +1965,379 @@ function updateAssetReview(project, fields) {
1930
1965
  return { ok: true, message: `Marked ${fields.assetId} ${fields.reviewState}`, asset_id: fields.assetId, review_state: fields.reviewState };
1931
1966
  }
1932
1967
 
1968
+ // src/server/agentClaims.ts
1969
+ import { createHash as createHash2, randomBytes, timingSafeEqual } from "node:crypto";
1970
+ var defaultTtlSeconds = 20 * 60;
1971
+ var idleAfterSeconds = 5 * 60;
1972
+ var staleAfterSeconds = 15 * 60;
1973
+ var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
1974
+ var AgentClaimError = class extends Error {
1975
+ constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
1976
+ super(message);
1977
+ this.status = status;
1978
+ this.code = code;
1979
+ this.conflicts = conflicts;
1980
+ }
1981
+ status;
1982
+ code;
1983
+ conflicts;
1984
+ };
1985
+ function isAgentClaimError(error) {
1986
+ return error instanceof AgentClaimError;
1987
+ }
1988
+ function parseClaimTtl(value) {
1989
+ if (!value) return defaultTtlSeconds;
1990
+ const match = value.trim().match(/^(\d+)(s|m|h)?$/);
1991
+ if (!match) throw new AgentClaimError(`Invalid claim ttl: ${value}`);
1992
+ const amount = Number(match[1]);
1993
+ const unit = match[2] || "s";
1994
+ const multiplier = unit === "h" ? 3600 : unit === "m" ? 60 : 1;
1995
+ const seconds = amount * multiplier;
1996
+ if (!Number.isInteger(seconds) || seconds < 30 || seconds > 24 * 60 * 60) {
1997
+ throw new AgentClaimError("Claim ttl must be between 30 seconds and 24 hours");
1998
+ }
1999
+ return seconds;
2000
+ }
2001
+ function randomId(prefix) {
2002
+ return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
2003
+ }
2004
+ function tokenHash(token) {
2005
+ return createHash2("sha256").update(token).digest("hex");
2006
+ }
2007
+ function safeEqual(a, b) {
2008
+ const left = Buffer.from(a);
2009
+ const right = Buffer.from(b);
2010
+ return left.length === right.length && timingSafeEqual(left, right);
2011
+ }
2012
+ function expiresAtFrom(timestamp, ttlSeconds) {
2013
+ return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
2014
+ }
2015
+ function metadataJson(metadata2) {
2016
+ return metadata2 ? JSON.stringify(metadata2) : null;
2017
+ }
2018
+ function parseMetadata(value) {
2019
+ if (typeof value !== "string" || !value) return void 0;
2020
+ try {
2021
+ const parsed = JSON.parse(value);
2022
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
2023
+ } catch {
2024
+ return void 0;
2025
+ }
2026
+ }
2027
+ function derivedState(row, now = /* @__PURE__ */ new Date()) {
2028
+ if (row.status !== "active") return row.status === "expired" ? "expired" : "stale";
2029
+ if (new Date(row.expires_at).getTime() <= now.getTime()) return "expired";
2030
+ const ageSeconds = Math.max(0, Math.floor((now.getTime() - new Date(row.heartbeat_at).getTime()) / 1e3));
2031
+ if (ageSeconds >= staleAfterSeconds) return "stale";
2032
+ if (ageSeconds >= idleAfterSeconds) return "idle";
2033
+ return "active";
2034
+ }
2035
+ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
2036
+ const heartbeatAt = String(row.heartbeat_at);
2037
+ return {
2038
+ id: String(row.id),
2039
+ project: String(row.project_id),
2040
+ channel: typeof row.channel === "string" ? row.channel : void 0,
2041
+ scope_type: String(row.scope_type),
2042
+ target_id: String(row.target_id),
2043
+ target_title: typeof row.target_title === "string" ? row.target_title : void 0,
2044
+ agent_id: typeof row.agent_id === "string" ? row.agent_id : void 0,
2045
+ agent_name: String(row.agent_name),
2046
+ agent_kind: String(row.agent_kind),
2047
+ thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
2048
+ status: String(row.status),
2049
+ created_at: String(row.created_at),
2050
+ heartbeat_at: heartbeatAt,
2051
+ expires_at: String(row.expires_at),
2052
+ released_at: typeof row.released_at === "string" ? row.released_at : void 0,
2053
+ revoked_at: typeof row.revoked_at === "string" ? row.revoked_at : void 0,
2054
+ revoked_by: typeof row.revoked_by === "string" ? row.revoked_by : void 0,
2055
+ override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
2056
+ metadata: parseMetadata(row.metadata_json),
2057
+ heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
2058
+ derived_state: derivedState({
2059
+ expires_at: String(row.expires_at),
2060
+ heartbeat_at: heartbeatAt,
2061
+ status: String(row.status)
2062
+ }, now)
2063
+ };
2064
+ }
2065
+ function eventToRow(row) {
2066
+ return {
2067
+ claim_id: String(row.claim_id),
2068
+ event_type: String(row.event_type),
2069
+ actor: typeof row.actor === "string" ? row.actor : void 0,
2070
+ message: typeof row.message === "string" ? row.message : void 0,
2071
+ created_at: String(row.created_at),
2072
+ metadata: parseMetadata(row.metadata_json)
2073
+ };
2074
+ }
2075
+ function ensureProject3(database, project) {
2076
+ const timestamp = nowIso();
2077
+ database.prepare(`
2078
+ insert into projects (id, product, created_at, updated_at)
2079
+ values (?, ?, ?, ?)
2080
+ on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
2081
+ `).run(project, project, timestamp, timestamp);
2082
+ }
2083
+ function recordEvent(database, claimId, eventType, actor, message, metadata2) {
2084
+ database.prepare(`
2085
+ insert into agent_claim_events (id, claim_id, event_type, actor, message, created_at, metadata_json)
2086
+ values (?, ?, ?, ?, ?, ?, ?)
2087
+ `).run(randomId("claim_event"), claimId, eventType, actor || null, message || null, nowIso(), metadataJson(metadata2));
2088
+ }
2089
+ function expireActiveClaims(database) {
2090
+ const timestamp = nowIso();
2091
+ const expired = database.prepare(`
2092
+ select id from agent_claims where status = 'active' and expires_at <= ?
2093
+ `).all(timestamp);
2094
+ if (expired.length === 0) return;
2095
+ database.prepare(`
2096
+ update agent_claims set status = 'expired' where status = 'active' and expires_at <= ?
2097
+ `).run(timestamp);
2098
+ for (const claim of expired) recordEvent(database, claim.id, "expired", "system", "Claim expired after missed heartbeat.");
2099
+ }
2100
+ function normalizeScope(scopeType) {
2101
+ if (!scopes.has(scopeType)) throw new AgentClaimError(`Unsupported claim scope: ${scopeType}`);
2102
+ return scopeType;
2103
+ }
2104
+ function channelOverlaps(left, right) {
2105
+ return !left || !right || left === right;
2106
+ }
2107
+ function claimOverlaps(candidate, existing) {
2108
+ if (candidate.project !== existing.project) return false;
2109
+ if (!channelOverlaps(candidate.channel, existing.channel)) return false;
2110
+ if (candidate.scope_type === existing.scope_type && candidate.target_id === existing.target_id) return true;
2111
+ return candidate.scope_type === "project_channel" || existing.scope_type === "project_channel";
2112
+ }
2113
+ function activeClaims(database, project) {
2114
+ const rows = project ? database.prepare("select * from agent_claims where project_id = ? and status = 'active' order by heartbeat_at desc").all(project) : database.prepare("select * from agent_claims where status = 'active' order by project_id, heartbeat_at desc").all();
2115
+ return rows.map((row) => rowToClaim(row));
2116
+ }
2117
+ function findClaimById(database, claimId, project) {
2118
+ const row = project ? database.prepare("select * from agent_claims where project_id = ? and id = ?").get(project, claimId) : database.prepare("select * from agent_claims where id = ?").get(claimId);
2119
+ return row ? rowToClaim(row) : null;
2120
+ }
2121
+ function findClaimRowByToken(database, claimToken) {
2122
+ const claimId = claimToken.split(".")[0];
2123
+ const row = database.prepare("select * from agent_claims where id = ?").get(claimId);
2124
+ if (!row) return null;
2125
+ return safeEqual(String(row.token_hash), tokenHash(claimToken)) ? row : null;
2126
+ }
2127
+ function denied(code, message, conflicts = []) {
2128
+ return { ok: false, code, message, conflicts };
2129
+ }
2130
+ function scopeAllowsWrite(claim, scopeType, targetId, writeKind) {
2131
+ if (claim.scope_type === scopeType && claim.target_id === targetId) return true;
2132
+ if (claim.scope_type === "project_channel") return true;
2133
+ if (claim.scope_type === "lineage_workspace" && scopeType === "lineage_workspace") return claim.target_id === targetId;
2134
+ if (claim.scope_type === "content_queue_lane" && writeKind === "content_queue_next") return true;
2135
+ return false;
2136
+ }
2137
+ function createAgentClaim(fields) {
2138
+ const project = fields.project.trim();
2139
+ const targetId = fields.targetId.trim();
2140
+ const agentName = fields.agentName.trim();
2141
+ if (!project) throw new AgentClaimError("Agent claim requires project");
2142
+ if (!targetId) throw new AgentClaimError("Agent claim requires target");
2143
+ if (!agentName) throw new AgentClaimError("Agent claim requires agent name");
2144
+ const scopeType = normalizeScope(fields.scopeType);
2145
+ const ttlSeconds = fields.ttlSeconds || defaultTtlSeconds;
2146
+ const database = lineageDb();
2147
+ try {
2148
+ ensureProject3(database, project);
2149
+ expireActiveClaims(database);
2150
+ const candidate = { project, channel: fields.channel?.trim() || void 0, scope_type: scopeType, target_id: targetId };
2151
+ const conflicts = activeClaims(database, project).filter((claim2) => claimOverlaps(candidate, claim2));
2152
+ if (conflicts.length > 0 && !fields.force) {
2153
+ throw new AgentClaimError("Target already has an active overlapping agent claim.", 409, "target_already_claimed", conflicts);
2154
+ }
2155
+ if (conflicts.length > 0 && !fields.reason?.trim()) {
2156
+ throw new AgentClaimError("Overriding an active claim requires --reason.", 400, "override_reason_required", conflicts);
2157
+ }
2158
+ const timestamp = nowIso();
2159
+ for (const conflict of conflicts) {
2160
+ database.prepare(`
2161
+ update agent_claims
2162
+ set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
2163
+ where id = ? and status = 'active'
2164
+ `).run(timestamp, agentName, fields.reason || null, conflict.id);
2165
+ recordEvent(database, conflict.id, "revoked", agentName, fields.reason || "Revoked by forced claim takeover.");
2166
+ recordEvent(database, conflict.id, "conflict", agentName, `Overridden by ${agentName}.`, { new_claim_target: targetId });
2167
+ }
2168
+ const id = randomId("claim");
2169
+ const secret = randomBytes(24).toString("base64url");
2170
+ const claimToken = `${id}.${secret}`;
2171
+ const expiresAt = expiresAtFrom(timestamp, ttlSeconds);
2172
+ database.prepare(`
2173
+ insert into agent_claims (
2174
+ id, token_hash, project_id, channel, scope_type, target_id, target_title, agent_id, agent_name,
2175
+ agent_kind, thread_id, status, created_at, heartbeat_at, expires_at, metadata_json
2176
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
2177
+ `).run(
2178
+ id,
2179
+ tokenHash(claimToken),
2180
+ project,
2181
+ candidate.channel || null,
2182
+ scopeType,
2183
+ targetId,
2184
+ fields.targetTitle?.trim() || null,
2185
+ fields.agentId?.trim() || null,
2186
+ agentName,
2187
+ fields.agentKind?.trim() || "codex",
2188
+ fields.threadId?.trim() || null,
2189
+ timestamp,
2190
+ timestamp,
2191
+ expiresAt,
2192
+ metadataJson(fields.metadata)
2193
+ );
2194
+ recordEvent(database, id, "created", agentName, `Claimed ${scopeType} ${targetId}.`, { ttl_seconds: ttlSeconds });
2195
+ const claim = findClaimById(database, id);
2196
+ return { ok: true, claim, claim_token: claimToken, conflicts_revoked: conflicts.map((conflict) => conflict.id) };
2197
+ } finally {
2198
+ database.close();
2199
+ }
2200
+ }
2201
+ function listAgentClaims(project) {
2202
+ const database = lineageDb();
2203
+ try {
2204
+ expireActiveClaims(database);
2205
+ const rows = project ? database.prepare("select * from agent_claims where project_id = ? order by status, heartbeat_at desc").all(project) : database.prepare("select * from agent_claims order by project_id, status, heartbeat_at desc").all();
2206
+ return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
2207
+ } finally {
2208
+ database.close();
2209
+ }
2210
+ }
2211
+ function inspectAgentClaim(claimId, project) {
2212
+ const database = lineageDb();
2213
+ try {
2214
+ expireActiveClaims(database);
2215
+ const claim = findClaimById(database, claimId, project);
2216
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
2217
+ const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
2218
+ return { ok: true, claim, events: events.map(eventToRow) };
2219
+ } finally {
2220
+ database.close();
2221
+ }
2222
+ }
2223
+ function heartbeatAgentClaim(claimToken, ttlSeconds = defaultTtlSeconds) {
2224
+ const database = lineageDb();
2225
+ try {
2226
+ expireActiveClaims(database);
2227
+ const row = findClaimRowByToken(database, claimToken);
2228
+ if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
2229
+ const claim = rowToClaim(row);
2230
+ if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
2231
+ const timestamp = nowIso();
2232
+ database.prepare("update agent_claims set heartbeat_at = ?, expires_at = ? where id = ?").run(timestamp, expiresAtFrom(timestamp, ttlSeconds), claim.id);
2233
+ recordEvent(database, claim.id, "heartbeat", claim.agent_name, "Claim heartbeat received.");
2234
+ return { ok: true, claim: findClaimById(database, claim.id) };
2235
+ } finally {
2236
+ database.close();
2237
+ }
2238
+ }
2239
+ function releaseAgentClaim(claimToken) {
2240
+ const database = lineageDb();
2241
+ try {
2242
+ expireActiveClaims(database);
2243
+ const row = findClaimRowByToken(database, claimToken);
2244
+ if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
2245
+ const claim = rowToClaim(row);
2246
+ if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
2247
+ const timestamp = nowIso();
2248
+ database.prepare("update agent_claims set status = 'released', released_at = ? where id = ?").run(timestamp, claim.id);
2249
+ recordEvent(database, claim.id, "released", claim.agent_name, "Claim released by token holder.");
2250
+ return { ok: true, claim: findClaimById(database, claim.id) };
2251
+ } finally {
2252
+ database.close();
2253
+ }
2254
+ }
2255
+ function releaseStaleAgentClaim(project, claimId, fields) {
2256
+ if (!fields.confirmWrite) throw new AgentClaimError("Releasing a stale agent claim requires confirmWrite=true.", 400, "confirm_write_required");
2257
+ if (!fields.reason?.trim()) throw new AgentClaimError("Releasing a stale agent claim requires a reason.", 400, "reason_required");
2258
+ const database = lineageDb();
2259
+ try {
2260
+ expireActiveClaims(database);
2261
+ const claim = findClaimById(database, claimId, project);
2262
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
2263
+ if (claim.status !== "active" || claim.derived_state !== "stale") {
2264
+ throw new AgentClaimError("Only stale active claims can be released without the claim token.", 409, "claim_not_stale", [claim]);
2265
+ }
2266
+ const timestamp = nowIso();
2267
+ database.prepare("update agent_claims set status = 'released', released_at = ?, revoked_by = ?, override_reason = ? where id = ?").run(timestamp, fields.actor || "human", fields.reason, claim.id);
2268
+ recordEvent(database, claim.id, "released", fields.actor || "human", fields.reason);
2269
+ return { ok: true, claim: findClaimById(database, claim.id) };
2270
+ } finally {
2271
+ database.close();
2272
+ }
2273
+ }
2274
+ function revokeAgentClaim(project, claimId, fields) {
2275
+ if (!fields.confirmWrite) throw new AgentClaimError("Revoking an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
2276
+ if (!fields.reason?.trim()) throw new AgentClaimError("Revoking an agent claim requires a reason.", 400, "reason_required");
2277
+ const database = lineageDb();
2278
+ try {
2279
+ expireActiveClaims(database);
2280
+ const claim = findClaimById(database, claimId, project);
2281
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
2282
+ const timestamp = nowIso();
2283
+ database.prepare(`
2284
+ update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
2285
+ where id = ?
2286
+ `).run(timestamp, fields.actor || "human", fields.reason, claim.id);
2287
+ recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
2288
+ return { ok: true, claim: findClaimById(database, claim.id) };
2289
+ } finally {
2290
+ database.close();
2291
+ }
2292
+ }
2293
+ function transferAgentClaim(project, claimId, fields) {
2294
+ if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
2295
+ const toAgentName = fields.toAgentName.trim();
2296
+ if (!toAgentName) throw new AgentClaimError("Transfer requires toAgentName.", 400, "agent_name_required");
2297
+ const database = lineageDb();
2298
+ try {
2299
+ expireActiveClaims(database);
2300
+ const claim = findClaimById(database, claimId, project);
2301
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
2302
+ if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
2303
+ database.prepare("update agent_claims set agent_name = ? where id = ?").run(toAgentName, claim.id);
2304
+ recordEvent(database, claim.id, "transferred", fields.actor || "human", fields.reason || `Transferred claim to ${toAgentName}.`, { to_agent_name: toAgentName });
2305
+ return { ok: true, claim: findClaimById(database, claim.id) };
2306
+ } finally {
2307
+ database.close();
2308
+ }
2309
+ }
2310
+ function validateAgentClaimForWrite(fields) {
2311
+ if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
2312
+ return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
2313
+ }
2314
+ if (!fields.claimToken) return denied("claim_required", "Mutating agent write requires a matching claim token.");
2315
+ const database = lineageDb();
2316
+ try {
2317
+ expireActiveClaims(database);
2318
+ const row = findClaimRowByToken(database, fields.claimToken);
2319
+ if (!row) return denied("claim_token_invalid", "Unknown or invalid claim token.");
2320
+ const claim = rowToClaim(row);
2321
+ if (claim.status !== "active") return denied("claim_not_active", `Agent claim is ${claim.status}.`);
2322
+ if (new Date(claim.expires_at).getTime() <= Date.now()) return denied("claim_expired", "Agent claim has expired.");
2323
+ if (claim.project !== fields.project) return denied("claim_project_mismatch", `Claim project ${claim.project} does not match ${fields.project}.`, [claim]);
2324
+ if (fields.channel && claim.channel && claim.channel !== fields.channel) {
2325
+ return denied("claim_channel_mismatch", `Claim channel ${claim.channel} does not match ${fields.channel}.`, [claim]);
2326
+ }
2327
+ if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
2328
+ return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
2329
+ }
2330
+ recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
2331
+ danger_level: fields.dangerLevel,
2332
+ target_id: fields.targetId,
2333
+ write_kind: fields.writeKind
2334
+ });
2335
+ return { ok: true, claim, warnings: [] };
2336
+ } finally {
2337
+ database.close();
2338
+ }
2339
+ }
2340
+
1933
2341
  // src/server/assetLineageHandoff.ts
1934
2342
  var publicPackageCommand = "npx @mean-weasel/lineage";
1935
2343
  function shellQuote(value) {
@@ -1987,9 +2395,23 @@ function getLineageBrief(project, rootAssetId) {
1987
2395
  function linkSelectedLineageChild(project, fields) {
1988
2396
  const next = getLineageNextAsset(project, fields.rootAssetId);
1989
2397
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
2398
+ if (fields.confirmWrite) {
2399
+ const validation = validateAgentClaimForWrite({
2400
+ channel: next.next_asset.channel,
2401
+ claimToken: fields.claimToken,
2402
+ confirmWrite: fields.confirmWrite,
2403
+ dangerLevel: "enforce",
2404
+ project,
2405
+ scopeType: "lineage_workspace",
2406
+ targetId: lineageWorkspaceId(project, next.root_asset_id),
2407
+ writeKind: "link_child"
2408
+ });
2409
+ if (!validation.ok) throw new AgentClaimError(validation.message, validation.code === "claim_required" ? 401 : 403, validation.code, validation.conflicts);
2410
+ }
1990
2411
  const result = linkLineageAssets(project, {
1991
2412
  childAssetId: fields.childAssetId,
1992
2413
  confirmWrite: fields.confirmWrite,
2414
+ claimToken: fields.claimToken,
1993
2415
  parentAssetId: next.next_asset.asset_id
1994
2416
  });
1995
2417
  return {
@@ -2867,7 +3289,7 @@ var AssetSelectionError = class extends Error {
2867
3289
  function isAssetSelectionError(error) {
2868
3290
  return error instanceof AssetSelectionError;
2869
3291
  }
2870
- function ensureProject3(database, project) {
3292
+ function ensureProject4(database, project) {
2871
3293
  const timestamp = nowIso();
2872
3294
  database.prepare(`
2873
3295
  insert into projects (id, product, created_at, updated_at)
@@ -2953,7 +3375,7 @@ function getCurrentSet(database, project) {
2953
3375
  return found;
2954
3376
  }
2955
3377
  function ensureSelectionSet(database, project, fields) {
2956
- ensureProject3(database, project);
3378
+ ensureProject4(database, project);
2957
3379
  const timestamp = nowIso();
2958
3380
  const id = setId(project, fields.kind, fields.key);
2959
3381
  database.prepare(`
@@ -3183,6 +3605,78 @@ function assetSelectionRouter(projectFrom2) {
3183
3605
  return router;
3184
3606
  }
3185
3607
 
3608
+ // src/server/agentClaimRoutes.ts
3609
+ function claimTokenFromRequest(req) {
3610
+ const header = req.header("X-Lineage-Claim-Token");
3611
+ if (header) return header;
3612
+ return typeof req.body?.claimToken === "string" ? req.body.claimToken : void 0;
3613
+ }
3614
+ function stringBody(req, key) {
3615
+ const value = req.body[key];
3616
+ return typeof value === "string" ? value : void 0;
3617
+ }
3618
+ function boolBody(req, key) {
3619
+ return req.body[key] === true;
3620
+ }
3621
+ function registerAgentClaimRoutes(app2, projectFrom2, asyncRoute2) {
3622
+ app2.get("/api/agent-claims", asyncRoute2((req, res) => {
3623
+ const project = typeof req.query.project === "string" ? req.query.project : void 0;
3624
+ res.json(listAgentClaims(project));
3625
+ }));
3626
+ app2.post("/api/agent-claims", asyncRoute2((req, res) => {
3627
+ res.json(createAgentClaim({
3628
+ agentId: stringBody(req, "agentId"),
3629
+ agentKind: stringBody(req, "agentKind"),
3630
+ agentName: stringBody(req, "agentName") || stringBody(req, "agent_name") || "",
3631
+ channel: stringBody(req, "channel"),
3632
+ force: boolBody(req, "force"),
3633
+ metadata: typeof req.body.metadata === "object" && req.body.metadata !== null ? req.body.metadata : void 0,
3634
+ project: projectFrom2({ body: req.body, query: req.query }),
3635
+ reason: stringBody(req, "reason"),
3636
+ scopeType: stringBody(req, "scopeType") || stringBody(req, "scope_type") || "",
3637
+ targetId: stringBody(req, "targetId") || stringBody(req, "target_id") || "",
3638
+ targetTitle: stringBody(req, "targetTitle") || stringBody(req, "target_title"),
3639
+ threadId: stringBody(req, "threadId") || stringBody(req, "thread_id"),
3640
+ ttlSeconds: parseClaimTtl(stringBody(req, "ttl"))
3641
+ }));
3642
+ }));
3643
+ app2.get("/api/agent-claims/:claimId", asyncRoute2((req, res) => {
3644
+ res.json(inspectAgentClaim(req.params.claimId, typeof req.query.project === "string" ? req.query.project : void 0));
3645
+ }));
3646
+ app2.post("/api/agent-claims/:claimId/heartbeat", asyncRoute2((req, res) => {
3647
+ const claimToken = claimTokenFromRequest(req);
3648
+ if (!claimToken) throw new AgentClaimError("Heartbeat requires claimToken", 400, "claim_token_required");
3649
+ res.json(heartbeatAgentClaim(claimToken, parseClaimTtl(stringBody(req, "ttl"))));
3650
+ }));
3651
+ app2.post("/api/agent-claims/:claimId/release", asyncRoute2((req, res) => {
3652
+ const claimToken = claimTokenFromRequest(req);
3653
+ if (!claimToken) throw new AgentClaimError("Release requires claimToken", 400, "claim_token_required");
3654
+ res.json(releaseAgentClaim(claimToken));
3655
+ }));
3656
+ app2.post("/api/agent-claims/:claimId/release-stale", asyncRoute2((req, res) => {
3657
+ res.json(releaseStaleAgentClaim(projectFrom2({ body: req.body, query: req.query }), req.params.claimId, {
3658
+ actor: stringBody(req, "actor") || "human",
3659
+ confirmWrite: boolBody(req, "confirmWrite"),
3660
+ reason: stringBody(req, "reason")
3661
+ }));
3662
+ }));
3663
+ app2.post("/api/agent-claims/:claimId/revoke", asyncRoute2((req, res) => {
3664
+ res.json(revokeAgentClaim(projectFrom2({ body: req.body, query: req.query }), req.params.claimId, {
3665
+ actor: stringBody(req, "actor") || "human",
3666
+ confirmWrite: boolBody(req, "confirmWrite"),
3667
+ reason: stringBody(req, "reason")
3668
+ }));
3669
+ }));
3670
+ app2.post("/api/agent-claims/:claimId/transfer", asyncRoute2((req, res) => {
3671
+ res.json(transferAgentClaim(projectFrom2({ body: req.body, query: req.query }), req.params.claimId, {
3672
+ actor: stringBody(req, "actor") || "human",
3673
+ confirmWrite: boolBody(req, "confirmWrite"),
3674
+ reason: stringBody(req, "reason"),
3675
+ toAgentName: stringBody(req, "toAgentName") || stringBody(req, "to_agent_name") || ""
3676
+ }));
3677
+ }));
3678
+ }
3679
+
3186
3680
  // src/server/adapters/adapterSettings.ts
3187
3681
  var AdapterSettingsError = class extends Error {
3188
3682
  constructor(message, status = 400) {
@@ -3227,7 +3721,7 @@ var definitions = [
3227
3721
  secret_ref: null
3228
3722
  }
3229
3723
  ];
3230
- function ensureProject4(database, project) {
3724
+ function ensureProject5(database, project) {
3231
3725
  const timestamp = nowIso();
3232
3726
  database.prepare(`
3233
3727
  insert into projects (id, product, created_at, updated_at)
@@ -3241,7 +3735,7 @@ function definitionFor(adapterType, provider) {
3241
3735
  return definition;
3242
3736
  }
3243
3737
  function seedDefaults(database, project) {
3244
- ensureProject4(database, project);
3738
+ ensureProject5(database, project);
3245
3739
  const timestamp = nowIso();
3246
3740
  for (const definition of definitions) {
3247
3741
  database.prepare(`
@@ -3500,7 +3994,7 @@ var ContentBatchError = class extends Error {
3500
3994
  function isContentBatchError(error) {
3501
3995
  return error instanceof ContentBatchError;
3502
3996
  }
3503
- function ensureProject5(database, project) {
3997
+ function ensureProject6(database, project) {
3504
3998
  const timestamp = nowIso();
3505
3999
  database.prepare(`
3506
4000
  insert into projects (id, product, created_at, updated_at)
@@ -3628,7 +4122,7 @@ function createContentBatch(project, fields) {
3628
4122
  const database = lineageDb();
3629
4123
  const timestamp = nowIso();
3630
4124
  try {
3631
- ensureProject5(database, project);
4125
+ ensureProject6(database, project);
3632
4126
  database.prepare(`
3633
4127
  insert into content_batches (id, project_id, title, campaign, channel, status, notes, created_at, updated_at)
3634
4128
  values (?, ?, ?, ?, ?, 'active', ?, ?, ?)
@@ -3650,7 +4144,7 @@ function createContentPost(project, fields) {
3650
4144
  const database = lineageDb();
3651
4145
  const timestamp = nowIso();
3652
4146
  try {
3653
- ensureProject5(database, project);
4147
+ ensureProject6(database, project);
3654
4148
  const batch = database.prepare("select id from content_batches where project_id = ? and id = ?").get(project, batchId);
3655
4149
  if (!batch) throw new ContentBatchError(`Unknown content batch: ${batchId}`, 404);
3656
4150
  database.prepare(`
@@ -3675,6 +4169,30 @@ function findContentPost(database, project, postId) {
3675
4169
  if (!row) throw new ContentBatchError(`Unknown content post: ${postId}`, 404);
3676
4170
  return postFromRow(row, assetsForPost(database, project, postId));
3677
4171
  }
4172
+ function requireContentPostClaim(project, post, claimToken, writeKind) {
4173
+ if (!claimToken && !hasActiveContentPostClaim(project, post)) return;
4174
+ const validation = validateAgentClaimForWrite({
4175
+ channel: post.channel,
4176
+ claimToken,
4177
+ confirmWrite: true,
4178
+ dangerLevel: "enforce",
4179
+ project,
4180
+ scopeType: "content_post",
4181
+ targetId: post.id,
4182
+ writeKind
4183
+ });
4184
+ if (!validation.ok) {
4185
+ const status = validation.code === "claim_required" || validation.code === "claim_token_invalid" ? 401 : 409;
4186
+ throw new AgentClaimError(validation.message, status, validation.code, validation.conflicts);
4187
+ }
4188
+ }
4189
+ function hasActiveContentPostClaim(project, post) {
4190
+ return listAgentClaims(project).claims.some((claim) => {
4191
+ if (claim.project !== project || claim.status !== "active" || claim.derived_state === "expired") return false;
4192
+ if (claim.scope_type === "content_post") return claim.target_id === post.id;
4193
+ return claim.scope_type === "project_channel" && (!claim.channel || claim.channel === post.channel);
4194
+ });
4195
+ }
3678
4196
  function listContentPosts(project, filters = {}) {
3679
4197
  const database = lineageDb();
3680
4198
  try {
@@ -3697,6 +4215,7 @@ function updateContentPost(project, fields) {
3697
4215
  const timestamp = nowIso();
3698
4216
  try {
3699
4217
  const current = findContentPost(database, project, postId);
4218
+ if (fields.phase) requireContentPostClaim(project, current, fields.claimToken, "content_post_phase");
3700
4219
  if (fields.batchId) {
3701
4220
  const batch = database.prepare("select id from content_batches where project_id = ? and id = ?").get(project, fields.batchId);
3702
4221
  if (!batch) throw new ContentBatchError(`Unknown content batch: ${fields.batchId}`, 404);
@@ -3734,7 +4253,8 @@ function attachContentPostAsset(project, fields) {
3734
4253
  const database = lineageDb();
3735
4254
  const timestamp = nowIso();
3736
4255
  try {
3737
- findContentPost(database, project, postId);
4256
+ const current = findContentPost(database, project, postId);
4257
+ requireContentPostClaim(project, current, fields.claimToken, "content_post_attach_asset");
3738
4258
  database.prepare(`
3739
4259
  insert into content_post_assets (id, project_id, post_id, asset_id, role, notes, attached_at)
3740
4260
  values (?, ?, ?, ?, ?, ?, ?)
@@ -3752,6 +4272,8 @@ function detachContentPostAsset(project, fields) {
3752
4272
  if (!fields.confirmWrite) return { ok: true, dryRun: true, message: `Would detach ${assetId} from ${postId}`, preview: { postId, assetId, role } };
3753
4273
  const database = lineageDb();
3754
4274
  try {
4275
+ const current = findContentPost(database, project, postId);
4276
+ requireContentPostClaim(project, current, fields.claimToken, "content_post_detach_asset");
3755
4277
  database.prepare("delete from content_post_assets where project_id = ? and post_id = ? and asset_id = ? and role = ?").run(project, postId, assetId, role);
3756
4278
  return { ok: true, message: `Detached ${assetId} from ${postId}`, post: findContentPost(database, project, postId) };
3757
4279
  } finally {
@@ -4302,13 +4824,19 @@ function getContentOpsQueue(project) {
4302
4824
  }
4303
4825
 
4304
4826
  // src/server/contentBatchRoutes.ts
4305
- function stringBody(req, key) {
4827
+ function stringBody2(req, key) {
4306
4828
  const value = req.body[key];
4307
4829
  return typeof value === "string" ? value : void 0;
4308
4830
  }
4309
- function boolBody(req, key) {
4831
+ function boolBody2(req, key) {
4310
4832
  return req.body[key] === true;
4311
4833
  }
4834
+ function claimTokenFromRequest2(req) {
4835
+ const header = req.header("X-Lineage-Claim-Token");
4836
+ if (header) return header;
4837
+ const value = req.body.claimToken;
4838
+ return typeof value === "string" ? value : void 0;
4839
+ }
4312
4840
  function bodyProjectShape(req) {
4313
4841
  return { body: req.body, query: req.query };
4314
4842
  }
@@ -4319,22 +4847,22 @@ function contentBatchRouter(projectFrom2) {
4319
4847
  });
4320
4848
  router.post("/batches", (req, res) => {
4321
4849
  res.json(createContentBatch(projectFrom2(bodyProjectShape(req)), {
4322
- batchId: stringBody(req, "batchId") || stringBody(req, "id") || "",
4323
- campaign: stringBody(req, "campaign"),
4324
- channel: stringBody(req, "channel"),
4325
- confirmWrite: boolBody(req, "confirmWrite"),
4326
- notes: stringBody(req, "notes"),
4327
- title: stringBody(req, "title") || ""
4850
+ batchId: stringBody2(req, "batchId") || stringBody2(req, "id") || "",
4851
+ campaign: stringBody2(req, "campaign"),
4852
+ channel: stringBody2(req, "channel"),
4853
+ confirmWrite: boolBody2(req, "confirmWrite"),
4854
+ notes: stringBody2(req, "notes"),
4855
+ title: stringBody2(req, "title") || ""
4328
4856
  }));
4329
4857
  });
4330
4858
  router.post("/import/demo", (req, res) => {
4331
- const kind = stringBody(req, "kind");
4859
+ const kind = stringBody2(req, "kind");
4332
4860
  res.json(importDemoContentBatch(projectFrom2(bodyProjectShape(req)), {
4333
- batchId: stringBody(req, "batchId") || "",
4334
- campaign: stringBody(req, "campaign"),
4335
- confirmWrite: boolBody(req, "confirmWrite"),
4861
+ batchId: stringBody2(req, "batchId") || "",
4862
+ campaign: stringBody2(req, "campaign"),
4863
+ confirmWrite: boolBody2(req, "confirmWrite"),
4336
4864
  kind: kind === "concepts" || kind === "drafts" || kind === "all" ? kind : "all",
4337
- title: stringBody(req, "title")
4865
+ title: stringBody2(req, "title")
4338
4866
  }));
4339
4867
  });
4340
4868
  router.get("/target", (req, res) => {
@@ -4345,13 +4873,13 @@ function contentBatchRouter(projectFrom2) {
4345
4873
  });
4346
4874
  router.post("/target", (req, res) => {
4347
4875
  res.json(setContentTarget(projectFrom2(bodyProjectShape(req)), {
4348
- confirmWrite: boolBody(req, "confirmWrite"),
4349
- notes: stringBody(req, "notes"),
4350
- postId: stringBody(req, "postId") || ""
4876
+ confirmWrite: boolBody2(req, "confirmWrite"),
4877
+ notes: stringBody2(req, "notes"),
4878
+ postId: stringBody2(req, "postId") || ""
4351
4879
  }));
4352
4880
  });
4353
4881
  router.post("/target/clear", (req, res) => {
4354
- res.json(clearContentTarget(projectFrom2(bodyProjectShape(req)), boolBody(req, "confirmWrite")));
4882
+ res.json(clearContentTarget(projectFrom2(bodyProjectShape(req)), boolBody2(req, "confirmWrite")));
4355
4883
  });
4356
4884
  router.get("/batches/:batchId", (req, res) => {
4357
4885
  res.json(getContentBatch(projectFrom2(req), req.params.batchId));
@@ -4365,52 +4893,55 @@ function contentBatchRouter(projectFrom2) {
4365
4893
  });
4366
4894
  router.post("/posts", (req, res) => {
4367
4895
  res.json(createContentPost(projectFrom2(bodyProjectShape(req)), {
4368
- batchId: stringBody(req, "batchId") || "",
4369
- body: stringBody(req, "body"),
4370
- campaign: stringBody(req, "campaign"),
4371
- channel: stringBody(req, "channel") || "",
4372
- confirmWrite: boolBody(req, "confirmWrite"),
4373
- cta: stringBody(req, "cta"),
4374
- notes: stringBody(req, "notes"),
4375
- phase: stringBody(req, "phase"),
4376
- postId: stringBody(req, "postId") || stringBody(req, "id") || "",
4377
- sourcePath: stringBody(req, "sourcePath"),
4378
- title: stringBody(req, "title") || ""
4896
+ batchId: stringBody2(req, "batchId") || "",
4897
+ body: stringBody2(req, "body"),
4898
+ campaign: stringBody2(req, "campaign"),
4899
+ channel: stringBody2(req, "channel") || "",
4900
+ confirmWrite: boolBody2(req, "confirmWrite"),
4901
+ cta: stringBody2(req, "cta"),
4902
+ notes: stringBody2(req, "notes"),
4903
+ phase: stringBody2(req, "phase"),
4904
+ postId: stringBody2(req, "postId") || stringBody2(req, "id") || "",
4905
+ sourcePath: stringBody2(req, "sourcePath"),
4906
+ title: stringBody2(req, "title") || ""
4379
4907
  }));
4380
4908
  });
4381
4909
  router.post("/posts/:postId", (req, res) => {
4382
4910
  res.json(updateContentPost(projectFrom2(bodyProjectShape(req)), {
4383
- batchId: stringBody(req, "batchId"),
4384
- body: stringBody(req, "body"),
4385
- campaign: stringBody(req, "campaign"),
4386
- channel: stringBody(req, "channel"),
4387
- confirmWrite: boolBody(req, "confirmWrite"),
4388
- cta: stringBody(req, "cta"),
4389
- notes: stringBody(req, "notes"),
4390
- phase: stringBody(req, "phase"),
4391
- postedAt: stringBody(req, "postedAt"),
4911
+ batchId: stringBody2(req, "batchId"),
4912
+ body: stringBody2(req, "body"),
4913
+ campaign: stringBody2(req, "campaign"),
4914
+ channel: stringBody2(req, "channel"),
4915
+ confirmWrite: boolBody2(req, "confirmWrite"),
4916
+ claimToken: claimTokenFromRequest2(req),
4917
+ cta: stringBody2(req, "cta"),
4918
+ notes: stringBody2(req, "notes"),
4919
+ phase: stringBody2(req, "phase"),
4920
+ postedAt: stringBody2(req, "postedAt"),
4392
4921
  postId: req.params.postId,
4393
- scheduledAt: stringBody(req, "scheduledAt"),
4394
- sourcePath: stringBody(req, "sourcePath"),
4395
- title: stringBody(req, "title"),
4396
- url: stringBody(req, "url")
4922
+ scheduledAt: stringBody2(req, "scheduledAt"),
4923
+ sourcePath: stringBody2(req, "sourcePath"),
4924
+ title: stringBody2(req, "title"),
4925
+ url: stringBody2(req, "url")
4397
4926
  }));
4398
4927
  });
4399
4928
  router.post("/posts/:postId/assets", (req, res) => {
4400
4929
  res.json(attachContentPostAsset(projectFrom2(bodyProjectShape(req)), {
4401
- assetId: stringBody(req, "assetId") || "",
4402
- confirmWrite: boolBody(req, "confirmWrite"),
4403
- notes: stringBody(req, "notes"),
4930
+ assetId: stringBody2(req, "assetId") || "",
4931
+ confirmWrite: boolBody2(req, "confirmWrite"),
4932
+ claimToken: claimTokenFromRequest2(req),
4933
+ notes: stringBody2(req, "notes"),
4404
4934
  postId: req.params.postId,
4405
- role: stringBody(req, "role")
4935
+ role: stringBody2(req, "role")
4406
4936
  }));
4407
4937
  });
4408
4938
  router.post("/posts/:postId/assets/detach", (req, res) => {
4409
4939
  res.json(detachContentPostAsset(projectFrom2(bodyProjectShape(req)), {
4410
- assetId: stringBody(req, "assetId") || "",
4411
- confirmWrite: boolBody(req, "confirmWrite"),
4940
+ assetId: stringBody2(req, "assetId") || "",
4941
+ confirmWrite: boolBody2(req, "confirmWrite"),
4942
+ claimToken: claimTokenFromRequest2(req),
4412
4943
  postId: req.params.postId,
4413
- role: stringBody(req, "role")
4944
+ role: stringBody2(req, "role")
4414
4945
  }));
4415
4946
  });
4416
4947
  return router;
@@ -4526,7 +5057,7 @@ function listImageGenerationJobs(project = defaultProject, fields = {}) {
4526
5057
  }
4527
5058
 
4528
5059
  // src/server/assetLineageDemo.ts
4529
- import { createHash as createHash2 } from "node:crypto";
5060
+ import { createHash as createHash3 } from "node:crypto";
4530
5061
  import { copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
4531
5062
  import { tmpdir } from "node:os";
4532
5063
  import { dirname as dirname3, join as join8, posix } from "node:path";
@@ -4609,7 +5140,7 @@ function swissifierMediaState(manifest) {
4609
5140
  return { invalid, missing };
4610
5141
  }
4611
5142
  function sha256Hex(input) {
4612
- return createHash2("sha256").update(input).digest("hex");
5143
+ return createHash3("sha256").update(input).digest("hex");
4613
5144
  }
4614
5145
  function safeTarEntryName(rawName) {
4615
5146
  const stripped = rawName.replace(/\0.*$/, "").replace(/^\.\//, "");
@@ -4870,7 +5401,7 @@ function svg(label, fill, stroke) {
4870
5401
  `;
4871
5402
  }
4872
5403
  function assetIdFor(asset) {
4873
- return `local-${createHash2("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
5404
+ return `local-${createHash3("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
4874
5405
  }
4875
5406
  function demoAssetIds() {
4876
5407
  return Object.fromEntries(demoAssets.map((asset) => [asset.key, assetIdFor(asset)]));
@@ -4916,7 +5447,7 @@ function upsertDemoAssets(project, ids) {
4916
5447
  ids[asset.key],
4917
5448
  project,
4918
5449
  demoRelativePath(project, asset),
4919
- createHash2("sha256").update(body).digest("hex"),
5450
+ createHash3("sha256").update(body).digest("hex"),
4920
5451
  asset.label,
4921
5452
  asset.channel,
4922
5453
  Buffer.byteLength(body),
@@ -5222,6 +5753,7 @@ app.post("/api/assets/lookup", asyncRoute((req, res) => {
5222
5753
  res.json(lookupAssets(projectFrom(req), Array.isArray(req.body.assetIds) ? req.body.assetIds.map(String) : []));
5223
5754
  }));
5224
5755
  registerAdapterRoutes(app, projectFrom, asyncRoute);
5756
+ registerAgentClaimRoutes(app, projectFrom, asyncRoute);
5225
5757
  app.use("/api/content", contentBatchRouter(projectFrom));
5226
5758
  app.use("/api/selections", assetSelectionRouter(projectFrom));
5227
5759
  app.get("/api/generation/jobs", asyncRoute((req, res) => {
@@ -5315,7 +5847,8 @@ app.post(
5315
5847
  linkSelectedLineageChild(projectFrom(req), {
5316
5848
  rootAssetId: typeof req.body.rootAssetId === "string" ? req.body.rootAssetId : void 0,
5317
5849
  childAssetId: String(req.body.childAssetId || ""),
5318
- confirmWrite: req.body.confirmWrite === true
5850
+ confirmWrite: req.body.confirmWrite === true,
5851
+ claimToken: claimTokenFromRequest(req)
5319
5852
  })
5320
5853
  );
5321
5854
  })
@@ -5539,6 +6072,10 @@ app.use((error, _req, res, _next) => {
5539
6072
  res.status(error.status).json({ error: error.message });
5540
6073
  return;
5541
6074
  }
6075
+ if (isAgentClaimError(error)) {
6076
+ res.status(error.status).json({ error: error.code, message: error.message, conflicts: error.conflicts });
6077
+ return;
6078
+ }
5542
6079
  if (isLineageWorkspaceError(error)) {
5543
6080
  res.status(error.status).json({ error: error.message });
5544
6081
  return;