@mean-weasel/lineage 0.1.3 → 0.1.5

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);
@@ -1552,6 +1587,410 @@ function activeLineageWorkspaceRoot(project) {
1552
1587
  }
1553
1588
  }
1554
1589
 
1590
+ // src/server/agentClaims.ts
1591
+ import { createHash as createHash2, randomBytes, timingSafeEqual } from "node:crypto";
1592
+ var defaultTtlSeconds = 20 * 60;
1593
+ var idleAfterSeconds = 5 * 60;
1594
+ var staleAfterSeconds = 15 * 60;
1595
+ var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
1596
+ var AgentClaimError = class extends Error {
1597
+ constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
1598
+ super(message);
1599
+ this.status = status;
1600
+ this.code = code;
1601
+ this.conflicts = conflicts;
1602
+ }
1603
+ status;
1604
+ code;
1605
+ conflicts;
1606
+ };
1607
+ function isAgentClaimError(error) {
1608
+ return error instanceof AgentClaimError;
1609
+ }
1610
+ function parseClaimTtl(value) {
1611
+ if (!value) return defaultTtlSeconds;
1612
+ const match = value.trim().match(/^(\d+)(s|m|h)?$/);
1613
+ if (!match) throw new AgentClaimError(`Invalid claim ttl: ${value}`);
1614
+ const amount = Number(match[1]);
1615
+ const unit = match[2] || "s";
1616
+ const multiplier = unit === "h" ? 3600 : unit === "m" ? 60 : 1;
1617
+ const seconds = amount * multiplier;
1618
+ if (!Number.isInteger(seconds) || seconds < 30 || seconds > 24 * 60 * 60) {
1619
+ throw new AgentClaimError("Claim ttl must be between 30 seconds and 24 hours");
1620
+ }
1621
+ return seconds;
1622
+ }
1623
+ function randomId(prefix) {
1624
+ return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
1625
+ }
1626
+ function tokenHash(token) {
1627
+ return createHash2("sha256").update(token).digest("hex");
1628
+ }
1629
+ function safeEqual(a, b) {
1630
+ const left = Buffer.from(a);
1631
+ const right = Buffer.from(b);
1632
+ return left.length === right.length && timingSafeEqual(left, right);
1633
+ }
1634
+ function expiresAtFrom(timestamp, ttlSeconds) {
1635
+ return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
1636
+ }
1637
+ function metadataJson(metadata2) {
1638
+ return metadata2 ? JSON.stringify(metadata2) : null;
1639
+ }
1640
+ function parseMetadata(value) {
1641
+ if (typeof value !== "string" || !value) return void 0;
1642
+ try {
1643
+ const parsed = JSON.parse(value);
1644
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1645
+ } catch {
1646
+ return void 0;
1647
+ }
1648
+ }
1649
+ function derivedState(row, now = /* @__PURE__ */ new Date()) {
1650
+ if (row.status !== "active") return row.status === "expired" ? "expired" : "stale";
1651
+ if (new Date(row.expires_at).getTime() <= now.getTime()) return "expired";
1652
+ const ageSeconds = Math.max(0, Math.floor((now.getTime() - new Date(row.heartbeat_at).getTime()) / 1e3));
1653
+ if (ageSeconds >= staleAfterSeconds) return "stale";
1654
+ if (ageSeconds >= idleAfterSeconds) return "idle";
1655
+ return "active";
1656
+ }
1657
+ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
1658
+ const heartbeatAt = String(row.heartbeat_at);
1659
+ return {
1660
+ id: String(row.id),
1661
+ project: String(row.project_id),
1662
+ channel: typeof row.channel === "string" ? row.channel : void 0,
1663
+ scope_type: String(row.scope_type),
1664
+ target_id: String(row.target_id),
1665
+ target_title: typeof row.target_title === "string" ? row.target_title : void 0,
1666
+ agent_id: typeof row.agent_id === "string" ? row.agent_id : void 0,
1667
+ agent_name: String(row.agent_name),
1668
+ agent_kind: String(row.agent_kind),
1669
+ thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
1670
+ status: String(row.status),
1671
+ created_at: String(row.created_at),
1672
+ heartbeat_at: heartbeatAt,
1673
+ expires_at: String(row.expires_at),
1674
+ released_at: typeof row.released_at === "string" ? row.released_at : void 0,
1675
+ revoked_at: typeof row.revoked_at === "string" ? row.revoked_at : void 0,
1676
+ revoked_by: typeof row.revoked_by === "string" ? row.revoked_by : void 0,
1677
+ override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
1678
+ metadata: parseMetadata(row.metadata_json),
1679
+ heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
1680
+ derived_state: derivedState({
1681
+ expires_at: String(row.expires_at),
1682
+ heartbeat_at: heartbeatAt,
1683
+ status: String(row.status)
1684
+ }, now)
1685
+ };
1686
+ }
1687
+ function eventToRow(row) {
1688
+ return {
1689
+ claim_id: String(row.claim_id),
1690
+ event_type: String(row.event_type),
1691
+ actor: typeof row.actor === "string" ? row.actor : void 0,
1692
+ message: typeof row.message === "string" ? row.message : void 0,
1693
+ created_at: String(row.created_at),
1694
+ metadata: parseMetadata(row.metadata_json)
1695
+ };
1696
+ }
1697
+ function ensureProject3(database, project) {
1698
+ const timestamp = nowIso();
1699
+ database.prepare(`
1700
+ insert into projects (id, product, created_at, updated_at)
1701
+ values (?, ?, ?, ?)
1702
+ on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
1703
+ `).run(project, project, timestamp, timestamp);
1704
+ }
1705
+ function recordEvent(database, claimId, eventType, actor, message, metadata2) {
1706
+ database.prepare(`
1707
+ insert into agent_claim_events (id, claim_id, event_type, actor, message, created_at, metadata_json)
1708
+ values (?, ?, ?, ?, ?, ?, ?)
1709
+ `).run(randomId("claim_event"), claimId, eventType, actor || null, message || null, nowIso(), metadataJson(metadata2));
1710
+ }
1711
+ function expireActiveClaims(database) {
1712
+ const timestamp = nowIso();
1713
+ const expired = database.prepare(`
1714
+ select id from agent_claims where status = 'active' and expires_at <= ?
1715
+ `).all(timestamp);
1716
+ if (expired.length === 0) return;
1717
+ database.prepare(`
1718
+ update agent_claims set status = 'expired' where status = 'active' and expires_at <= ?
1719
+ `).run(timestamp);
1720
+ for (const claim of expired) recordEvent(database, claim.id, "expired", "system", "Claim expired after missed heartbeat.");
1721
+ }
1722
+ function normalizeScope(scopeType) {
1723
+ if (!scopes.has(scopeType)) throw new AgentClaimError(`Unsupported claim scope: ${scopeType}`);
1724
+ return scopeType;
1725
+ }
1726
+ function channelOverlaps(left, right) {
1727
+ return !left || !right || left === right;
1728
+ }
1729
+ function claimOverlaps(candidate, existing) {
1730
+ if (candidate.project !== existing.project) return false;
1731
+ if (!channelOverlaps(candidate.channel, existing.channel)) return false;
1732
+ if (candidate.scope_type === existing.scope_type && candidate.target_id === existing.target_id) return true;
1733
+ return candidate.scope_type === "project_channel" || existing.scope_type === "project_channel";
1734
+ }
1735
+ function activeClaims(database, project) {
1736
+ 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();
1737
+ return rows.map((row) => rowToClaim(row));
1738
+ }
1739
+ function findClaimById(database, claimId, project) {
1740
+ 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);
1741
+ return row ? rowToClaim(row) : null;
1742
+ }
1743
+ function findClaimRowByToken(database, claimToken) {
1744
+ const claimId = claimToken.split(".")[0];
1745
+ const row = database.prepare("select * from agent_claims where id = ?").get(claimId);
1746
+ if (!row) return null;
1747
+ return safeEqual(String(row.token_hash), tokenHash(claimToken)) ? row : null;
1748
+ }
1749
+ function denied(code, message, conflicts = []) {
1750
+ return { ok: false, code, message, conflicts };
1751
+ }
1752
+ function scopeAllowsWrite(claim, scopeType, targetId, writeKind) {
1753
+ if (claim.scope_type === scopeType && claim.target_id === targetId) return true;
1754
+ if (claim.scope_type === "project_channel") return true;
1755
+ if (claim.scope_type === "lineage_workspace" && scopeType === "lineage_workspace") return claim.target_id === targetId;
1756
+ if (claim.scope_type === "content_queue_lane" && writeKind === "content_queue_next") return true;
1757
+ return false;
1758
+ }
1759
+ function createAgentClaim(fields) {
1760
+ const project = fields.project.trim();
1761
+ const targetId = fields.targetId.trim();
1762
+ const agentName = fields.agentName.trim();
1763
+ if (!project) throw new AgentClaimError("Agent claim requires project");
1764
+ if (!targetId) throw new AgentClaimError("Agent claim requires target");
1765
+ if (!agentName) throw new AgentClaimError("Agent claim requires agent name");
1766
+ const scopeType = normalizeScope(fields.scopeType);
1767
+ const ttlSeconds = fields.ttlSeconds || defaultTtlSeconds;
1768
+ const database = lineageDb();
1769
+ try {
1770
+ ensureProject3(database, project);
1771
+ expireActiveClaims(database);
1772
+ const candidate = { project, channel: fields.channel?.trim() || void 0, scope_type: scopeType, target_id: targetId };
1773
+ const conflicts = activeClaims(database, project).filter((claim2) => claimOverlaps(candidate, claim2));
1774
+ if (conflicts.length > 0 && !fields.force) {
1775
+ throw new AgentClaimError("Target already has an active overlapping agent claim.", 409, "target_already_claimed", conflicts);
1776
+ }
1777
+ if (conflicts.length > 0 && !fields.reason?.trim()) {
1778
+ throw new AgentClaimError("Overriding an active claim requires --reason.", 400, "override_reason_required", conflicts);
1779
+ }
1780
+ const timestamp = nowIso();
1781
+ for (const conflict of conflicts) {
1782
+ database.prepare(`
1783
+ update agent_claims
1784
+ set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
1785
+ where id = ? and status = 'active'
1786
+ `).run(timestamp, agentName, fields.reason || null, conflict.id);
1787
+ recordEvent(database, conflict.id, "revoked", agentName, fields.reason || "Revoked by forced claim takeover.");
1788
+ recordEvent(database, conflict.id, "conflict", agentName, `Overridden by ${agentName}.`, { new_claim_target: targetId });
1789
+ }
1790
+ const id = randomId("claim");
1791
+ const secret = randomBytes(24).toString("base64url");
1792
+ const claimToken = `${id}.${secret}`;
1793
+ const expiresAt = expiresAtFrom(timestamp, ttlSeconds);
1794
+ database.prepare(`
1795
+ insert into agent_claims (
1796
+ id, token_hash, project_id, channel, scope_type, target_id, target_title, agent_id, agent_name,
1797
+ agent_kind, thread_id, status, created_at, heartbeat_at, expires_at, metadata_json
1798
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
1799
+ `).run(
1800
+ id,
1801
+ tokenHash(claimToken),
1802
+ project,
1803
+ candidate.channel || null,
1804
+ scopeType,
1805
+ targetId,
1806
+ fields.targetTitle?.trim() || null,
1807
+ fields.agentId?.trim() || null,
1808
+ agentName,
1809
+ fields.agentKind?.trim() || "codex",
1810
+ fields.threadId?.trim() || null,
1811
+ timestamp,
1812
+ timestamp,
1813
+ expiresAt,
1814
+ metadataJson(fields.metadata)
1815
+ );
1816
+ recordEvent(database, id, "created", agentName, `Claimed ${scopeType} ${targetId}.`, { ttl_seconds: ttlSeconds });
1817
+ const claim = findClaimById(database, id);
1818
+ return { ok: true, claim, claim_token: claimToken, conflicts_revoked: conflicts.map((conflict) => conflict.id) };
1819
+ } finally {
1820
+ database.close();
1821
+ }
1822
+ }
1823
+ function listAgentClaims(project) {
1824
+ const database = lineageDb();
1825
+ try {
1826
+ expireActiveClaims(database);
1827
+ 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();
1828
+ return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
1829
+ } finally {
1830
+ database.close();
1831
+ }
1832
+ }
1833
+ function inspectAgentClaim(claimId, project) {
1834
+ const database = lineageDb();
1835
+ try {
1836
+ expireActiveClaims(database);
1837
+ const claim = findClaimById(database, claimId, project);
1838
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1839
+ const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
1840
+ return { ok: true, claim, events: events.map(eventToRow) };
1841
+ } finally {
1842
+ database.close();
1843
+ }
1844
+ }
1845
+ function heartbeatAgentClaim(claimToken, ttlSeconds = defaultTtlSeconds) {
1846
+ const database = lineageDb();
1847
+ try {
1848
+ expireActiveClaims(database);
1849
+ const row = findClaimRowByToken(database, claimToken);
1850
+ if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
1851
+ const claim = rowToClaim(row);
1852
+ if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
1853
+ const timestamp = nowIso();
1854
+ database.prepare("update agent_claims set heartbeat_at = ?, expires_at = ? where id = ?").run(timestamp, expiresAtFrom(timestamp, ttlSeconds), claim.id);
1855
+ recordEvent(database, claim.id, "heartbeat", claim.agent_name, "Claim heartbeat received.");
1856
+ return { ok: true, claim: findClaimById(database, claim.id) };
1857
+ } finally {
1858
+ database.close();
1859
+ }
1860
+ }
1861
+ function releaseAgentClaim(claimToken) {
1862
+ const database = lineageDb();
1863
+ try {
1864
+ expireActiveClaims(database);
1865
+ const row = findClaimRowByToken(database, claimToken);
1866
+ if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
1867
+ const claim = rowToClaim(row);
1868
+ if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
1869
+ const timestamp = nowIso();
1870
+ database.prepare("update agent_claims set status = 'released', released_at = ? where id = ?").run(timestamp, claim.id);
1871
+ recordEvent(database, claim.id, "released", claim.agent_name, "Claim released by token holder.");
1872
+ return { ok: true, claim: findClaimById(database, claim.id) };
1873
+ } finally {
1874
+ database.close();
1875
+ }
1876
+ }
1877
+ function releaseStaleAgentClaim(project, claimId, fields) {
1878
+ if (!fields.confirmWrite) throw new AgentClaimError("Releasing a stale agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1879
+ if (!fields.reason?.trim()) throw new AgentClaimError("Releasing a stale agent claim requires a reason.", 400, "reason_required");
1880
+ const database = lineageDb();
1881
+ try {
1882
+ expireActiveClaims(database);
1883
+ const claim = findClaimById(database, claimId, project);
1884
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1885
+ if (claim.status !== "active" || claim.derived_state !== "stale") {
1886
+ throw new AgentClaimError("Only stale active claims can be released without the claim token.", 409, "claim_not_stale", [claim]);
1887
+ }
1888
+ const timestamp = nowIso();
1889
+ 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);
1890
+ recordEvent(database, claim.id, "released", fields.actor || "human", fields.reason);
1891
+ return { ok: true, claim: findClaimById(database, claim.id) };
1892
+ } finally {
1893
+ database.close();
1894
+ }
1895
+ }
1896
+ function revokeAgentClaim(project, claimId, fields) {
1897
+ if (!fields.confirmWrite) throw new AgentClaimError("Revoking an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1898
+ if (!fields.reason?.trim()) throw new AgentClaimError("Revoking an agent claim requires a reason.", 400, "reason_required");
1899
+ const database = lineageDb();
1900
+ try {
1901
+ expireActiveClaims(database);
1902
+ const claim = findClaimById(database, claimId, project);
1903
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1904
+ const timestamp = nowIso();
1905
+ database.prepare(`
1906
+ update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
1907
+ where id = ?
1908
+ `).run(timestamp, fields.actor || "human", fields.reason, claim.id);
1909
+ recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
1910
+ return { ok: true, claim: findClaimById(database, claim.id) };
1911
+ } finally {
1912
+ database.close();
1913
+ }
1914
+ }
1915
+ function transferAgentClaim(project, claimId, fields) {
1916
+ if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1917
+ const toAgentName = fields.toAgentName.trim();
1918
+ if (!toAgentName) throw new AgentClaimError("Transfer requires toAgentName.", 400, "agent_name_required");
1919
+ const database = lineageDb();
1920
+ try {
1921
+ expireActiveClaims(database);
1922
+ const claim = findClaimById(database, claimId, project);
1923
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1924
+ if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
1925
+ database.prepare("update agent_claims set agent_name = ? where id = ?").run(toAgentName, claim.id);
1926
+ recordEvent(database, claim.id, "transferred", fields.actor || "human", fields.reason || `Transferred claim to ${toAgentName}.`, { to_agent_name: toAgentName });
1927
+ return { ok: true, claim: findClaimById(database, claim.id) };
1928
+ } finally {
1929
+ database.close();
1930
+ }
1931
+ }
1932
+ function validateAgentClaimForWrite(fields) {
1933
+ if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
1934
+ return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
1935
+ }
1936
+ if (!fields.claimToken) return denied("claim_required", "Mutating agent write requires a matching claim token.");
1937
+ const database = lineageDb();
1938
+ try {
1939
+ expireActiveClaims(database);
1940
+ const row = findClaimRowByToken(database, fields.claimToken);
1941
+ if (!row) return denied("claim_token_invalid", "Unknown or invalid claim token.");
1942
+ const claim = rowToClaim(row);
1943
+ if (claim.status !== "active") return denied("claim_not_active", `Agent claim is ${claim.status}.`);
1944
+ if (new Date(claim.expires_at).getTime() <= Date.now()) return denied("claim_expired", "Agent claim has expired.");
1945
+ if (claim.project !== fields.project) return denied("claim_project_mismatch", `Claim project ${claim.project} does not match ${fields.project}.`, [claim]);
1946
+ if (fields.channel && claim.channel && claim.channel !== fields.channel) {
1947
+ return denied("claim_channel_mismatch", `Claim channel ${claim.channel} does not match ${fields.channel}.`, [claim]);
1948
+ }
1949
+ if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
1950
+ return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
1951
+ }
1952
+ recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
1953
+ danger_level: fields.dangerLevel,
1954
+ target_id: fields.targetId,
1955
+ write_kind: fields.writeKind
1956
+ });
1957
+ return { ok: true, claim, warnings: [] };
1958
+ } finally {
1959
+ database.close();
1960
+ }
1961
+ }
1962
+
1963
+ // src/server/lineageClaimGuards.ts
1964
+ function channelOverlaps2(left, right) {
1965
+ return !left || !right || left === right;
1966
+ }
1967
+ function hasActiveLineageWorkspaceClaim(project, targetId, channel) {
1968
+ return listAgentClaims(project).claims.some((claim) => {
1969
+ if (claim.project !== project || claim.status !== "active" || claim.derived_state === "expired") return false;
1970
+ if (claim.scope_type === "lineage_workspace") return claim.target_id === targetId;
1971
+ return claim.scope_type === "project_channel" && channelOverlaps2(channel, claim.channel);
1972
+ });
1973
+ }
1974
+ function requireLineageWorkspaceClaimForWrite(fields) {
1975
+ if (!fields.confirmWrite) return;
1976
+ const targetId = lineageWorkspaceId(fields.project, fields.rootAssetId);
1977
+ if (!fields.claimToken && !hasActiveLineageWorkspaceClaim(fields.project, targetId, fields.channel)) return;
1978
+ const validation = validateAgentClaimForWrite({
1979
+ channel: fields.channel,
1980
+ claimToken: fields.claimToken,
1981
+ confirmWrite: fields.confirmWrite,
1982
+ dangerLevel: "enforce",
1983
+ project: fields.project,
1984
+ scopeType: "lineage_workspace",
1985
+ targetId,
1986
+ writeKind: fields.writeKind
1987
+ });
1988
+ if (!validation.ok) {
1989
+ const status = validation.code === "claim_required" || validation.code === "claim_token_invalid" ? 401 : 409;
1990
+ throw new AgentClaimError(validation.message, status, validation.code, validation.conflicts);
1991
+ }
1992
+ }
1993
+
1555
1994
  // src/server/assetLineage.ts
1556
1995
  var LineageError = class extends Error {
1557
1996
  constructor(message, status = 400) {
@@ -1646,6 +2085,45 @@ function rootFor(database, project, assetId) {
1646
2085
  }
1647
2086
  return assetId;
1648
2087
  }
2088
+ function explicitWorkspaceRoot(database, project, assetId) {
2089
+ const row = database.prepare(`
2090
+ select root_asset_id from lineage_workspaces
2091
+ where project_id = ? and root_asset_id = ? and status != 'archived'
2092
+ `).get(project, assetId);
2093
+ return row?.root_asset_id;
2094
+ }
2095
+ function nearestWorkspaceRoot(database, project, assetId) {
2096
+ let current = assetId;
2097
+ const seen = /* @__PURE__ */ new Set();
2098
+ while (!seen.has(current)) {
2099
+ seen.add(current);
2100
+ const explicit = explicitWorkspaceRoot(database, project, current);
2101
+ if (explicit) return explicit;
2102
+ const parent = parentOf(database, project, current);
2103
+ if (!parent) return current;
2104
+ current = parent;
2105
+ }
2106
+ return assetId;
2107
+ }
2108
+ function assetChannel(database, project, assetId) {
2109
+ const row = database.prepare("select channel from assets where project_id = ? and id = ?").get(project, assetId);
2110
+ return row?.channel;
2111
+ }
2112
+ function lineageWriteClaimContext(database, project, assetId) {
2113
+ return {
2114
+ channel: assetChannel(database, project, assetId),
2115
+ rootAssetId: nearestWorkspaceRoot(database, project, assetId)
2116
+ };
2117
+ }
2118
+ function getLineageWriteClaimContext(project, assetId) {
2119
+ const database = lineageDb();
2120
+ try {
2121
+ requireAsset2(database, project, assetId);
2122
+ return lineageWriteClaimContext(database, project, assetId);
2123
+ } finally {
2124
+ database.close();
2125
+ }
2126
+ }
1649
2127
  function latestSelectedRoot(database, project) {
1650
2128
  const row = database.prepare("select root_asset_id from asset_selections where project_id = ? order by selected_at desc limit 1").get(project);
1651
2129
  return row?.root_asset_id;
@@ -1676,6 +2154,20 @@ function linkLineageAssets(project, fields) {
1676
2154
  requireAsset2(database, project, fields.parentAssetId);
1677
2155
  requireAsset2(database, project, fields.childAssetId);
1678
2156
  if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
2157
+ const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
2158
+ try {
2159
+ requireLineageWorkspaceClaimForWrite({
2160
+ channel: claimContext.channel,
2161
+ claimToken: fields.claimToken,
2162
+ confirmWrite: fields.confirmWrite,
2163
+ project,
2164
+ rootAssetId: claimContext.rootAssetId,
2165
+ writeKind: "lineage_link"
2166
+ });
2167
+ } catch (error) {
2168
+ database.close();
2169
+ throw error;
2170
+ }
1679
2171
  const edge = {
1680
2172
  id: edgeId(project, fields.parentAssetId, fields.childAssetId),
1681
2173
  parent_asset_id: fields.parentAssetId,
@@ -1712,7 +2204,7 @@ function descendants(database, project, root) {
1712
2204
  function getLineageSnapshot(project, assetId) {
1713
2205
  const database = lineageDb();
1714
2206
  requireAsset2(database, project, assetId);
1715
- const root = rootFor(database, project, assetId);
2207
+ const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
1716
2208
  const edges = descendants(database, project, root);
1717
2209
  const ids = [.../* @__PURE__ */ new Set([root, ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])])];
1718
2210
  const placeholders2 = ids.map(() => "?").join(",");
@@ -1766,6 +2258,19 @@ function updateLineageLayout(project, fields) {
1766
2258
  const database = lineageDb();
1767
2259
  requireAsset2(database, project, fields.rootAssetId);
1768
2260
  for (const position of fields.positions) requireAsset2(database, project, position.assetId);
2261
+ try {
2262
+ requireLineageWorkspaceClaimForWrite({
2263
+ channel: assetChannel(database, project, fields.rootAssetId),
2264
+ claimToken: fields.claimToken,
2265
+ confirmWrite: fields.confirmWrite,
2266
+ project,
2267
+ rootAssetId: fields.rootAssetId,
2268
+ writeKind: "lineage_layout"
2269
+ });
2270
+ } catch (error) {
2271
+ database.close();
2272
+ throw error;
2273
+ }
1769
2274
  if (!fields.confirmWrite) {
1770
2275
  database.close();
1771
2276
  return { ok: true, dryRun: true, root_asset_id: fields.rootAssetId, positions: fields.positions };
@@ -1987,9 +2492,24 @@ function getLineageBrief(project, rootAssetId) {
1987
2492
  function linkSelectedLineageChild(project, fields) {
1988
2493
  const next = getLineageNextAsset(project, fields.rootAssetId);
1989
2494
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
2495
+ if (fields.confirmWrite) {
2496
+ const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
2497
+ const validation = validateAgentClaimForWrite({
2498
+ channel: claimContext.channel,
2499
+ claimToken: fields.claimToken,
2500
+ confirmWrite: fields.confirmWrite,
2501
+ dangerLevel: "enforce",
2502
+ project,
2503
+ scopeType: "lineage_workspace",
2504
+ targetId: lineageWorkspaceId(project, claimContext.rootAssetId),
2505
+ writeKind: "link_child"
2506
+ });
2507
+ if (!validation.ok) throw new AgentClaimError(validation.message, validation.code === "claim_required" ? 401 : 403, validation.code, validation.conflicts);
2508
+ }
1990
2509
  const result = linkLineageAssets(project, {
1991
2510
  childAssetId: fields.childAssetId,
1992
2511
  confirmWrite: fields.confirmWrite,
2512
+ claimToken: fields.claimToken,
1993
2513
  parentAssetId: next.next_asset.asset_id
1994
2514
  });
1995
2515
  return {
@@ -2007,6 +2527,10 @@ function requireAsset3(database, project, assetId) {
2007
2527
  const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
2008
2528
  if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
2009
2529
  }
2530
+ function assetChannel2(database, project, assetId) {
2531
+ const row = database.prepare("select channel from assets where project_id = ? and id = ?").get(project, assetId);
2532
+ return row?.channel;
2533
+ }
2010
2534
  function parentOf2(database, project, assetId) {
2011
2535
  const row = database.prepare("select parent_asset_id from asset_edges where project_id = ? and child_asset_id = ? order by created_at limit 1").get(project, assetId);
2012
2536
  return row?.parent_asset_id;
@@ -2053,6 +2577,19 @@ function removeLineageNode(project, fields) {
2053
2577
  database.close();
2054
2578
  throw new LineageError(`Asset ${fields.assetId} is not in lineage rooted at ${root}`, 404);
2055
2579
  }
2580
+ try {
2581
+ requireLineageWorkspaceClaimForWrite({
2582
+ channel: assetChannel2(database, project, root),
2583
+ claimToken: fields.claimToken,
2584
+ confirmWrite: fields.confirmWrite,
2585
+ project,
2586
+ rootAssetId: root,
2587
+ writeKind: "lineage_remove_node"
2588
+ });
2589
+ } catch (error) {
2590
+ database.close();
2591
+ throw error;
2592
+ }
2056
2593
  const parentEdges = snapshot.edges.filter((edge) => edge.child_asset_id === fields.assetId);
2057
2594
  const childEdges = snapshot.edges.filter((edge) => edge.parent_asset_id === fields.assetId);
2058
2595
  const removedIds = [...parentEdges, ...childEdges].map((edge) => edge.id);
@@ -2867,7 +3404,7 @@ var AssetSelectionError = class extends Error {
2867
3404
  function isAssetSelectionError(error) {
2868
3405
  return error instanceof AssetSelectionError;
2869
3406
  }
2870
- function ensureProject3(database, project) {
3407
+ function ensureProject4(database, project) {
2871
3408
  const timestamp = nowIso();
2872
3409
  database.prepare(`
2873
3410
  insert into projects (id, product, created_at, updated_at)
@@ -2953,7 +3490,7 @@ function getCurrentSet(database, project) {
2953
3490
  return found;
2954
3491
  }
2955
3492
  function ensureSelectionSet(database, project, fields) {
2956
- ensureProject3(database, project);
3493
+ ensureProject4(database, project);
2957
3494
  const timestamp = nowIso();
2958
3495
  const id = setId(project, fields.kind, fields.key);
2959
3496
  database.prepare(`
@@ -3183,6 +3720,78 @@ function assetSelectionRouter(projectFrom2) {
3183
3720
  return router;
3184
3721
  }
3185
3722
 
3723
+ // src/server/agentClaimRoutes.ts
3724
+ function claimTokenFromRequest(req) {
3725
+ const header = req.header("X-Lineage-Claim-Token");
3726
+ if (header) return header;
3727
+ return typeof req.body?.claimToken === "string" ? req.body.claimToken : void 0;
3728
+ }
3729
+ function stringBody(req, key) {
3730
+ const value = req.body[key];
3731
+ return typeof value === "string" ? value : void 0;
3732
+ }
3733
+ function boolBody(req, key) {
3734
+ return req.body[key] === true;
3735
+ }
3736
+ function registerAgentClaimRoutes(app2, projectFrom2, asyncRoute2) {
3737
+ app2.get("/api/agent-claims", asyncRoute2((req, res) => {
3738
+ const project = typeof req.query.project === "string" ? req.query.project : void 0;
3739
+ res.json(listAgentClaims(project));
3740
+ }));
3741
+ app2.post("/api/agent-claims", asyncRoute2((req, res) => {
3742
+ res.json(createAgentClaim({
3743
+ agentId: stringBody(req, "agentId"),
3744
+ agentKind: stringBody(req, "agentKind"),
3745
+ agentName: stringBody(req, "agentName") || stringBody(req, "agent_name") || "",
3746
+ channel: stringBody(req, "channel"),
3747
+ force: boolBody(req, "force"),
3748
+ metadata: typeof req.body.metadata === "object" && req.body.metadata !== null ? req.body.metadata : void 0,
3749
+ project: projectFrom2({ body: req.body, query: req.query }),
3750
+ reason: stringBody(req, "reason"),
3751
+ scopeType: stringBody(req, "scopeType") || stringBody(req, "scope_type") || "",
3752
+ targetId: stringBody(req, "targetId") || stringBody(req, "target_id") || "",
3753
+ targetTitle: stringBody(req, "targetTitle") || stringBody(req, "target_title"),
3754
+ threadId: stringBody(req, "threadId") || stringBody(req, "thread_id"),
3755
+ ttlSeconds: parseClaimTtl(stringBody(req, "ttl"))
3756
+ }));
3757
+ }));
3758
+ app2.get("/api/agent-claims/:claimId", asyncRoute2((req, res) => {
3759
+ res.json(inspectAgentClaim(req.params.claimId, typeof req.query.project === "string" ? req.query.project : void 0));
3760
+ }));
3761
+ app2.post("/api/agent-claims/:claimId/heartbeat", asyncRoute2((req, res) => {
3762
+ const claimToken = claimTokenFromRequest(req);
3763
+ if (!claimToken) throw new AgentClaimError("Heartbeat requires claimToken", 400, "claim_token_required");
3764
+ res.json(heartbeatAgentClaim(claimToken, parseClaimTtl(stringBody(req, "ttl"))));
3765
+ }));
3766
+ app2.post("/api/agent-claims/:claimId/release", asyncRoute2((req, res) => {
3767
+ const claimToken = claimTokenFromRequest(req);
3768
+ if (!claimToken) throw new AgentClaimError("Release requires claimToken", 400, "claim_token_required");
3769
+ res.json(releaseAgentClaim(claimToken));
3770
+ }));
3771
+ app2.post("/api/agent-claims/:claimId/release-stale", asyncRoute2((req, res) => {
3772
+ res.json(releaseStaleAgentClaim(projectFrom2({ body: req.body, query: req.query }), req.params.claimId, {
3773
+ actor: stringBody(req, "actor") || "human",
3774
+ confirmWrite: boolBody(req, "confirmWrite"),
3775
+ reason: stringBody(req, "reason")
3776
+ }));
3777
+ }));
3778
+ app2.post("/api/agent-claims/:claimId/revoke", asyncRoute2((req, res) => {
3779
+ res.json(revokeAgentClaim(projectFrom2({ body: req.body, query: req.query }), req.params.claimId, {
3780
+ actor: stringBody(req, "actor") || "human",
3781
+ confirmWrite: boolBody(req, "confirmWrite"),
3782
+ reason: stringBody(req, "reason")
3783
+ }));
3784
+ }));
3785
+ app2.post("/api/agent-claims/:claimId/transfer", asyncRoute2((req, res) => {
3786
+ res.json(transferAgentClaim(projectFrom2({ body: req.body, query: req.query }), req.params.claimId, {
3787
+ actor: stringBody(req, "actor") || "human",
3788
+ confirmWrite: boolBody(req, "confirmWrite"),
3789
+ reason: stringBody(req, "reason"),
3790
+ toAgentName: stringBody(req, "toAgentName") || stringBody(req, "to_agent_name") || ""
3791
+ }));
3792
+ }));
3793
+ }
3794
+
3186
3795
  // src/server/adapters/adapterSettings.ts
3187
3796
  var AdapterSettingsError = class extends Error {
3188
3797
  constructor(message, status = 400) {
@@ -3227,7 +3836,7 @@ var definitions = [
3227
3836
  secret_ref: null
3228
3837
  }
3229
3838
  ];
3230
- function ensureProject4(database, project) {
3839
+ function ensureProject5(database, project) {
3231
3840
  const timestamp = nowIso();
3232
3841
  database.prepare(`
3233
3842
  insert into projects (id, product, created_at, updated_at)
@@ -3241,7 +3850,7 @@ function definitionFor(adapterType, provider) {
3241
3850
  return definition;
3242
3851
  }
3243
3852
  function seedDefaults(database, project) {
3244
- ensureProject4(database, project);
3853
+ ensureProject5(database, project);
3245
3854
  const timestamp = nowIso();
3246
3855
  for (const definition of definitions) {
3247
3856
  database.prepare(`
@@ -3500,7 +4109,7 @@ var ContentBatchError = class extends Error {
3500
4109
  function isContentBatchError(error) {
3501
4110
  return error instanceof ContentBatchError;
3502
4111
  }
3503
- function ensureProject5(database, project) {
4112
+ function ensureProject6(database, project) {
3504
4113
  const timestamp = nowIso();
3505
4114
  database.prepare(`
3506
4115
  insert into projects (id, product, created_at, updated_at)
@@ -3628,7 +4237,7 @@ function createContentBatch(project, fields) {
3628
4237
  const database = lineageDb();
3629
4238
  const timestamp = nowIso();
3630
4239
  try {
3631
- ensureProject5(database, project);
4240
+ ensureProject6(database, project);
3632
4241
  database.prepare(`
3633
4242
  insert into content_batches (id, project_id, title, campaign, channel, status, notes, created_at, updated_at)
3634
4243
  values (?, ?, ?, ?, ?, 'active', ?, ?, ?)
@@ -3650,7 +4259,7 @@ function createContentPost(project, fields) {
3650
4259
  const database = lineageDb();
3651
4260
  const timestamp = nowIso();
3652
4261
  try {
3653
- ensureProject5(database, project);
4262
+ ensureProject6(database, project);
3654
4263
  const batch = database.prepare("select id from content_batches where project_id = ? and id = ?").get(project, batchId);
3655
4264
  if (!batch) throw new ContentBatchError(`Unknown content batch: ${batchId}`, 404);
3656
4265
  database.prepare(`
@@ -3675,6 +4284,30 @@ function findContentPost(database, project, postId) {
3675
4284
  if (!row) throw new ContentBatchError(`Unknown content post: ${postId}`, 404);
3676
4285
  return postFromRow(row, assetsForPost(database, project, postId));
3677
4286
  }
4287
+ function requireContentPostClaim(project, post, claimToken, writeKind) {
4288
+ if (!claimToken && !hasActiveContentPostClaim(project, post)) return;
4289
+ const validation = validateAgentClaimForWrite({
4290
+ channel: post.channel,
4291
+ claimToken,
4292
+ confirmWrite: true,
4293
+ dangerLevel: "enforce",
4294
+ project,
4295
+ scopeType: "content_post",
4296
+ targetId: post.id,
4297
+ writeKind
4298
+ });
4299
+ if (!validation.ok) {
4300
+ const status = validation.code === "claim_required" || validation.code === "claim_token_invalid" ? 401 : 409;
4301
+ throw new AgentClaimError(validation.message, status, validation.code, validation.conflicts);
4302
+ }
4303
+ }
4304
+ function hasActiveContentPostClaim(project, post) {
4305
+ return listAgentClaims(project).claims.some((claim) => {
4306
+ if (claim.project !== project || claim.status !== "active" || claim.derived_state === "expired") return false;
4307
+ if (claim.scope_type === "content_post") return claim.target_id === post.id;
4308
+ return claim.scope_type === "project_channel" && (!claim.channel || claim.channel === post.channel);
4309
+ });
4310
+ }
3678
4311
  function listContentPosts(project, filters = {}) {
3679
4312
  const database = lineageDb();
3680
4313
  try {
@@ -3697,6 +4330,7 @@ function updateContentPost(project, fields) {
3697
4330
  const timestamp = nowIso();
3698
4331
  try {
3699
4332
  const current = findContentPost(database, project, postId);
4333
+ requireContentPostClaim(project, current, fields.claimToken, fields.phase ? "content_post_phase" : "content_post_update");
3700
4334
  if (fields.batchId) {
3701
4335
  const batch = database.prepare("select id from content_batches where project_id = ? and id = ?").get(project, fields.batchId);
3702
4336
  if (!batch) throw new ContentBatchError(`Unknown content batch: ${fields.batchId}`, 404);
@@ -3734,7 +4368,8 @@ function attachContentPostAsset(project, fields) {
3734
4368
  const database = lineageDb();
3735
4369
  const timestamp = nowIso();
3736
4370
  try {
3737
- findContentPost(database, project, postId);
4371
+ const current = findContentPost(database, project, postId);
4372
+ requireContentPostClaim(project, current, fields.claimToken, "content_post_attach_asset");
3738
4373
  database.prepare(`
3739
4374
  insert into content_post_assets (id, project_id, post_id, asset_id, role, notes, attached_at)
3740
4375
  values (?, ?, ?, ?, ?, ?, ?)
@@ -3752,6 +4387,8 @@ function detachContentPostAsset(project, fields) {
3752
4387
  if (!fields.confirmWrite) return { ok: true, dryRun: true, message: `Would detach ${assetId} from ${postId}`, preview: { postId, assetId, role } };
3753
4388
  const database = lineageDb();
3754
4389
  try {
4390
+ const current = findContentPost(database, project, postId);
4391
+ requireContentPostClaim(project, current, fields.claimToken, "content_post_detach_asset");
3755
4392
  database.prepare("delete from content_post_assets where project_id = ? and post_id = ? and asset_id = ? and role = ?").run(project, postId, assetId, role);
3756
4393
  return { ok: true, message: `Detached ${assetId} from ${postId}`, post: findContentPost(database, project, postId) };
3757
4394
  } finally {
@@ -3867,7 +4504,8 @@ function setContentTarget(project, fields) {
3867
4504
  const database = lineageDb();
3868
4505
  const timestamp = nowIso();
3869
4506
  try {
3870
- findPost(database, project, postId);
4507
+ const post = findPost(database, project, postId);
4508
+ requireContentPostClaim(project, post, fields.claimToken, "content_target_set");
3871
4509
  database.prepare(`
3872
4510
  insert into content_targets (project_id, post_id, notes, selected_at, updated_at)
3873
4511
  values (?, ?, ?, ?, ?)
@@ -3880,10 +4518,19 @@ function setContentTarget(project, fields) {
3880
4518
  }
3881
4519
  return { ok: true, message: `Selected content target ${postId}`, ...getContentTarget(project) };
3882
4520
  }
3883
- function clearContentTarget(project, confirmWrite) {
4521
+ function clearContentTarget(project, confirmWrite, claimToken) {
3884
4522
  if (!confirmWrite) return { ok: true, dryRun: true, message: "Would clear selected content target" };
3885
4523
  const database = lineageDb();
3886
4524
  try {
4525
+ const row = database.prepare("select post_id from content_targets where project_id = ?").get(project);
4526
+ if (row?.post_id) {
4527
+ try {
4528
+ const post = findPost(database, project, String(row.post_id));
4529
+ requireContentPostClaim(project, post, claimToken, "content_target_clear");
4530
+ } catch (error) {
4531
+ if (!(error instanceof ContentBatchError && error.status === 404)) throw error;
4532
+ }
4533
+ }
3887
4534
  database.prepare("delete from content_targets where project_id = ?").run(project);
3888
4535
  } finally {
3889
4536
  database.close();
@@ -4302,13 +4949,19 @@ function getContentOpsQueue(project) {
4302
4949
  }
4303
4950
 
4304
4951
  // src/server/contentBatchRoutes.ts
4305
- function stringBody(req, key) {
4952
+ function stringBody2(req, key) {
4306
4953
  const value = req.body[key];
4307
4954
  return typeof value === "string" ? value : void 0;
4308
4955
  }
4309
- function boolBody(req, key) {
4956
+ function boolBody2(req, key) {
4310
4957
  return req.body[key] === true;
4311
4958
  }
4959
+ function claimTokenFromRequest2(req) {
4960
+ const header = req.header("X-Lineage-Claim-Token");
4961
+ if (header) return header;
4962
+ const value = req.body.claimToken;
4963
+ return typeof value === "string" ? value : void 0;
4964
+ }
4312
4965
  function bodyProjectShape(req) {
4313
4966
  return { body: req.body, query: req.query };
4314
4967
  }
@@ -4319,22 +4972,22 @@ function contentBatchRouter(projectFrom2) {
4319
4972
  });
4320
4973
  router.post("/batches", (req, res) => {
4321
4974
  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") || ""
4975
+ batchId: stringBody2(req, "batchId") || stringBody2(req, "id") || "",
4976
+ campaign: stringBody2(req, "campaign"),
4977
+ channel: stringBody2(req, "channel"),
4978
+ confirmWrite: boolBody2(req, "confirmWrite"),
4979
+ notes: stringBody2(req, "notes"),
4980
+ title: stringBody2(req, "title") || ""
4328
4981
  }));
4329
4982
  });
4330
4983
  router.post("/import/demo", (req, res) => {
4331
- const kind = stringBody(req, "kind");
4984
+ const kind = stringBody2(req, "kind");
4332
4985
  res.json(importDemoContentBatch(projectFrom2(bodyProjectShape(req)), {
4333
- batchId: stringBody(req, "batchId") || "",
4334
- campaign: stringBody(req, "campaign"),
4335
- confirmWrite: boolBody(req, "confirmWrite"),
4986
+ batchId: stringBody2(req, "batchId") || "",
4987
+ campaign: stringBody2(req, "campaign"),
4988
+ confirmWrite: boolBody2(req, "confirmWrite"),
4336
4989
  kind: kind === "concepts" || kind === "drafts" || kind === "all" ? kind : "all",
4337
- title: stringBody(req, "title")
4990
+ title: stringBody2(req, "title")
4338
4991
  }));
4339
4992
  });
4340
4993
  router.get("/target", (req, res) => {
@@ -4345,13 +4998,14 @@ function contentBatchRouter(projectFrom2) {
4345
4998
  });
4346
4999
  router.post("/target", (req, res) => {
4347
5000
  res.json(setContentTarget(projectFrom2(bodyProjectShape(req)), {
4348
- confirmWrite: boolBody(req, "confirmWrite"),
4349
- notes: stringBody(req, "notes"),
4350
- postId: stringBody(req, "postId") || ""
5001
+ claimToken: claimTokenFromRequest2(req),
5002
+ confirmWrite: boolBody2(req, "confirmWrite"),
5003
+ notes: stringBody2(req, "notes"),
5004
+ postId: stringBody2(req, "postId") || ""
4351
5005
  }));
4352
5006
  });
4353
5007
  router.post("/target/clear", (req, res) => {
4354
- res.json(clearContentTarget(projectFrom2(bodyProjectShape(req)), boolBody(req, "confirmWrite")));
5008
+ res.json(clearContentTarget(projectFrom2(bodyProjectShape(req)), boolBody2(req, "confirmWrite"), claimTokenFromRequest2(req)));
4355
5009
  });
4356
5010
  router.get("/batches/:batchId", (req, res) => {
4357
5011
  res.json(getContentBatch(projectFrom2(req), req.params.batchId));
@@ -4365,52 +5019,55 @@ function contentBatchRouter(projectFrom2) {
4365
5019
  });
4366
5020
  router.post("/posts", (req, res) => {
4367
5021
  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") || ""
5022
+ batchId: stringBody2(req, "batchId") || "",
5023
+ body: stringBody2(req, "body"),
5024
+ campaign: stringBody2(req, "campaign"),
5025
+ channel: stringBody2(req, "channel") || "",
5026
+ confirmWrite: boolBody2(req, "confirmWrite"),
5027
+ cta: stringBody2(req, "cta"),
5028
+ notes: stringBody2(req, "notes"),
5029
+ phase: stringBody2(req, "phase"),
5030
+ postId: stringBody2(req, "postId") || stringBody2(req, "id") || "",
5031
+ sourcePath: stringBody2(req, "sourcePath"),
5032
+ title: stringBody2(req, "title") || ""
4379
5033
  }));
4380
5034
  });
4381
5035
  router.post("/posts/:postId", (req, res) => {
4382
5036
  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"),
5037
+ batchId: stringBody2(req, "batchId"),
5038
+ body: stringBody2(req, "body"),
5039
+ campaign: stringBody2(req, "campaign"),
5040
+ channel: stringBody2(req, "channel"),
5041
+ confirmWrite: boolBody2(req, "confirmWrite"),
5042
+ claimToken: claimTokenFromRequest2(req),
5043
+ cta: stringBody2(req, "cta"),
5044
+ notes: stringBody2(req, "notes"),
5045
+ phase: stringBody2(req, "phase"),
5046
+ postedAt: stringBody2(req, "postedAt"),
4392
5047
  postId: req.params.postId,
4393
- scheduledAt: stringBody(req, "scheduledAt"),
4394
- sourcePath: stringBody(req, "sourcePath"),
4395
- title: stringBody(req, "title"),
4396
- url: stringBody(req, "url")
5048
+ scheduledAt: stringBody2(req, "scheduledAt"),
5049
+ sourcePath: stringBody2(req, "sourcePath"),
5050
+ title: stringBody2(req, "title"),
5051
+ url: stringBody2(req, "url")
4397
5052
  }));
4398
5053
  });
4399
5054
  router.post("/posts/:postId/assets", (req, res) => {
4400
5055
  res.json(attachContentPostAsset(projectFrom2(bodyProjectShape(req)), {
4401
- assetId: stringBody(req, "assetId") || "",
4402
- confirmWrite: boolBody(req, "confirmWrite"),
4403
- notes: stringBody(req, "notes"),
5056
+ assetId: stringBody2(req, "assetId") || "",
5057
+ confirmWrite: boolBody2(req, "confirmWrite"),
5058
+ claimToken: claimTokenFromRequest2(req),
5059
+ notes: stringBody2(req, "notes"),
4404
5060
  postId: req.params.postId,
4405
- role: stringBody(req, "role")
5061
+ role: stringBody2(req, "role")
4406
5062
  }));
4407
5063
  });
4408
5064
  router.post("/posts/:postId/assets/detach", (req, res) => {
4409
5065
  res.json(detachContentPostAsset(projectFrom2(bodyProjectShape(req)), {
4410
- assetId: stringBody(req, "assetId") || "",
4411
- confirmWrite: boolBody(req, "confirmWrite"),
5066
+ assetId: stringBody2(req, "assetId") || "",
5067
+ confirmWrite: boolBody2(req, "confirmWrite"),
5068
+ claimToken: claimTokenFromRequest2(req),
4412
5069
  postId: req.params.postId,
4413
- role: stringBody(req, "role")
5070
+ role: stringBody2(req, "role")
4414
5071
  }));
4415
5072
  });
4416
5073
  return router;
@@ -4526,7 +5183,7 @@ function listImageGenerationJobs(project = defaultProject, fields = {}) {
4526
5183
  }
4527
5184
 
4528
5185
  // src/server/assetLineageDemo.ts
4529
- import { createHash as createHash2 } from "node:crypto";
5186
+ import { createHash as createHash3 } from "node:crypto";
4530
5187
  import { copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
4531
5188
  import { tmpdir } from "node:os";
4532
5189
  import { dirname as dirname3, join as join8, posix } from "node:path";
@@ -4609,7 +5266,7 @@ function swissifierMediaState(manifest) {
4609
5266
  return { invalid, missing };
4610
5267
  }
4611
5268
  function sha256Hex(input) {
4612
- return createHash2("sha256").update(input).digest("hex");
5269
+ return createHash3("sha256").update(input).digest("hex");
4613
5270
  }
4614
5271
  function safeTarEntryName(rawName) {
4615
5272
  const stripped = rawName.replace(/\0.*$/, "").replace(/^\.\//, "");
@@ -4870,7 +5527,7 @@ function svg(label, fill, stroke) {
4870
5527
  `;
4871
5528
  }
4872
5529
  function assetIdFor(asset) {
4873
- return `local-${createHash2("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
5530
+ return `local-${createHash3("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
4874
5531
  }
4875
5532
  function demoAssetIds() {
4876
5533
  return Object.fromEntries(demoAssets.map((asset) => [asset.key, assetIdFor(asset)]));
@@ -4916,7 +5573,7 @@ function upsertDemoAssets(project, ids) {
4916
5573
  ids[asset.key],
4917
5574
  project,
4918
5575
  demoRelativePath(project, asset),
4919
- createHash2("sha256").update(body).digest("hex"),
5576
+ createHash3("sha256").update(body).digest("hex"),
4920
5577
  asset.label,
4921
5578
  asset.channel,
4922
5579
  Buffer.byteLength(body),
@@ -5222,6 +5879,7 @@ app.post("/api/assets/lookup", asyncRoute((req, res) => {
5222
5879
  res.json(lookupAssets(projectFrom(req), Array.isArray(req.body.assetIds) ? req.body.assetIds.map(String) : []));
5223
5880
  }));
5224
5881
  registerAdapterRoutes(app, projectFrom, asyncRoute);
5882
+ registerAgentClaimRoutes(app, projectFrom, asyncRoute);
5225
5883
  app.use("/api/content", contentBatchRouter(projectFrom));
5226
5884
  app.use("/api/selections", assetSelectionRouter(projectFrom));
5227
5885
  app.get("/api/generation/jobs", asyncRoute((req, res) => {
@@ -5283,13 +5941,14 @@ app.post(
5283
5941
  linkLineageAssets(projectFrom(req), {
5284
5942
  parentAssetId: String(req.body.parentAssetId || ""),
5285
5943
  childAssetId: String(req.body.childAssetId || ""),
5286
- confirmWrite: req.body.confirmWrite === true
5944
+ confirmWrite: req.body.confirmWrite === true,
5945
+ claimToken: claimTokenFromRequest(req)
5287
5946
  })
5288
5947
  );
5289
5948
  })
5290
5949
  );
5291
5950
  app.post("/api/lineage/remove-node", asyncRoute((req, res) => {
5292
- res.json(removeLineageNode(projectFrom(req), { assetId: String(req.body.assetId || ""), rootAssetId: typeof req.body.rootAssetId === "string" ? req.body.rootAssetId : void 0, confirmWrite: req.body.confirmWrite === true }));
5951
+ res.json(removeLineageNode(projectFrom(req), { assetId: String(req.body.assetId || ""), rootAssetId: typeof req.body.rootAssetId === "string" ? req.body.rootAssetId : void 0, confirmWrite: req.body.confirmWrite === true, claimToken: claimTokenFromRequest(req) }));
5293
5952
  }));
5294
5953
  app.post(
5295
5954
  "/api/lineage/layout",
@@ -5303,7 +5962,8 @@ app.post(
5303
5962
  x: Number(position.x),
5304
5963
  y: Number(position.y)
5305
5964
  })),
5306
- confirmWrite: req.body.confirmWrite === true
5965
+ confirmWrite: req.body.confirmWrite === true,
5966
+ claimToken: claimTokenFromRequest(req)
5307
5967
  })
5308
5968
  );
5309
5969
  })
@@ -5315,7 +5975,8 @@ app.post(
5315
5975
  linkSelectedLineageChild(projectFrom(req), {
5316
5976
  rootAssetId: typeof req.body.rootAssetId === "string" ? req.body.rootAssetId : void 0,
5317
5977
  childAssetId: String(req.body.childAssetId || ""),
5318
- confirmWrite: req.body.confirmWrite === true
5978
+ confirmWrite: req.body.confirmWrite === true,
5979
+ claimToken: claimTokenFromRequest(req)
5319
5980
  })
5320
5981
  );
5321
5982
  })
@@ -5539,6 +6200,10 @@ app.use((error, _req, res, _next) => {
5539
6200
  res.status(error.status).json({ error: error.message });
5540
6201
  return;
5541
6202
  }
6203
+ if (isAgentClaimError(error)) {
6204
+ res.status(error.status).json({ error: error.code, message: error.message, conflicts: error.conflicts });
6205
+ return;
6206
+ }
5542
6207
  if (isLineageWorkspaceError(error)) {
5543
6208
  res.status(error.status).json({ error: error.message });
5544
6209
  return;