@inneranimalmedia/agentsam-sdk 2.4.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +7 -6
  2. package/docs/AGENTSAM_WORKBENCH.md +30 -0
  3. package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
  4. package/docs/CAPABILITIES.md +5 -3
  5. package/docs/CLI_SHELL.md +57 -80
  6. package/docs/CMS_STUDIO.md +101 -0
  7. package/docs/CONTEXT.md +170 -0
  8. package/docs/LOCAL_OLLAMA.md +58 -0
  9. package/docs/MERKLE.md +19 -19
  10. package/docs/PORTABLE_CONTEXT.md +4 -3
  11. package/docs/PROJECT_CONFIG.md +72 -0
  12. package/docs/RELEASES.md +26 -2
  13. package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
  14. package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
  15. package/docs/SDK_WORKER.md +86 -0
  16. package/docs/SECURITY.md +59 -22
  17. package/docs/client-cms-editor.md +15 -0
  18. package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
  19. package/package.json +17 -4
  20. package/packages/identity/package.json +11 -2
  21. package/packages/identity/src/contracts/auth-config.js +98 -0
  22. package/packages/identity/src/index.js +1 -0
  23. package/packages/identity/src/oauth/README.md +2 -2
  24. package/packages/identity/src/oauth/credentials.js +11 -4
  25. package/packages/identity/src/oauth/iam-platform.js +3 -3
  26. package/packages/identity/src/providers/iam/index.js +7 -7
  27. package/packages/identity/src/providers/iam/oauth.js +6 -4
  28. package/packages/identity/src/providers/iam/profile.js +5 -5
  29. package/packages/identity/tests/auth-config.test.mjs +57 -0
  30. package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
  31. package/protocol/FILEMETA_V1.md +95 -0
  32. package/protocol/INSPECT_VIEWS_V1.md +40 -0
  33. package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
  34. package/protocol/MERKLE_V1.md +3 -1
  35. package/protocol/capabilities/manifest.json +32 -3
  36. package/protocol/capabilities/repository-snapshot.schema.json +1 -0
  37. package/protocol/context/context-budget.schema.json +41 -0
  38. package/protocol/context/context-item.schema.json +20 -0
  39. package/protocol/context/resolved-context-pack.schema.json +42 -0
  40. package/protocol/context/result-policy.schema.json +17 -0
  41. package/protocol/knowledge/context-pack.schema.json +33 -0
  42. package/protocol/knowledge/retrieval-query.schema.json +68 -13
  43. package/python/README.md +15 -7
  44. package/python/agentsam_sdk/cli.py +0 -21
  45. package/python/agentsam_sdk/tui/README.md +17 -12
  46. package/python/agentsam_sdk/tui/bootstrap.py +2 -2
  47. package/python/agentsam_sdk/tui/demo.py +25 -10
  48. package/python/agentsam_sdk/tui/onboarding.py +208 -0
  49. package/python/tests/test_tui_cli.py +7 -6
  50. package/skills/README.md +21 -0
  51. package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
  52. package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
  53. package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
  54. package/skills/agentsam-jr-dev/SKILL.md +232 -0
  55. package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
  56. package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
  57. package/skills/agentsam-progression-guard/SKILL.md +197 -0
  58. package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
  59. package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
  60. package/skills/catalog.json +53 -0
  61. package/src/capabilities/index.js +7 -0
  62. package/src/capabilities/repository-snapshot-view.js +238 -0
  63. package/src/capabilities/repository-snapshot.js +28 -14
  64. package/src/cli.js +108 -56
  65. package/src/commands/context.js +14 -2
  66. package/src/commands/db.js +4 -7
  67. package/src/commands/deploy.js +17 -31
  68. package/src/commands/interactive.js +21 -0
  69. package/src/commands/knowledge.js +27 -4
  70. package/src/commands/merkle-persist.js +118 -0
  71. package/src/commands/merkle.js +32 -17
  72. package/src/commands/models.js +107 -0
  73. package/src/commands/ollama.js +259 -0
  74. package/src/commands/preferences.js +102 -0
  75. package/src/commands/product.js +86 -16
  76. package/src/commands/security.js +3 -3
  77. package/src/commands/shell.js +71 -27
  78. package/src/commands/skills.js +66 -0
  79. package/src/commands/start-local.js +1 -1
  80. package/src/commands/tunnel.js +5 -4
  81. package/src/context/budget.js +58 -0
  82. package/src/context/compact.js +28 -0
  83. package/src/context/index.js +5 -0
  84. package/src/context/resolve.js +84 -0
  85. package/src/context/result-policy.js +66 -0
  86. package/src/index.js +16 -0
  87. package/src/indexing/execution-boundary.js +144 -0
  88. package/src/indexing/index.js +8 -0
  89. package/src/indexing/provider.js +41 -0
  90. package/src/knowledge/config.js +2 -2
  91. package/src/knowledge/context-pack.js +12 -1
  92. package/src/knowledge/contracts.js +9 -4
  93. package/src/knowledge/engine.js +7 -3
  94. package/src/knowledge/service/server.js +1 -1
  95. package/src/lib/bridge-client.js +7 -5
  96. package/src/lib/cli-preferences.js +74 -0
  97. package/src/lib/core-client.js +8 -8
  98. package/src/lib/deploy-receipt/index.js +5 -2
  99. package/src/lib/detect-context.js +6 -5
  100. package/src/lib/identity-scaffold.js +1 -1
  101. package/src/lib/local-scaffold.js +35 -33
  102. package/src/lib/local-status.js +9 -17
  103. package/src/lib/merkle/cloudflare-persistence.js +321 -0
  104. package/src/lib/merkle/filemeta.js +43 -0
  105. package/src/lib/merkle/git-ignore.js +24 -0
  106. package/src/lib/merkle/hash.js +1 -0
  107. package/src/lib/merkle/index.js +22 -0
  108. package/src/lib/merkle/persistence.js +72 -0
  109. package/src/lib/merkle/semantic.js +359 -0
  110. package/src/lib/merkle/snapshot.js +9 -3
  111. package/src/lib/merkle/tree.js +11 -6
  112. package/src/lib/project-config.js +227 -0
  113. package/src/lib/project-rules.js +68 -0
  114. package/src/lib/save-sdk-token.js +1 -1
  115. package/src/lib/slash-commands.js +2 -1
  116. package/src/lib/tools.js +11 -5
  117. package/src/security/index.js +1 -0
  118. package/src/security/inventory.js +4 -1
  119. package/src/security/render.js +27 -5
  120. package/src/security/scan.js +24 -9
  121. package/src/security/trust-boundary.js +24 -0
  122. package/src/skills/index.js +64 -0
  123. package/src/tools/index.js +1 -0
  124. package/src/tools/search.js +70 -0
  125. package/src/ui/ansi.js +1 -1
  126. package/src/ui/boot.js +56 -0
  127. package/src/ui/merkle/render.js +1 -0
  128. package/src/ui/runtime-activity.js +192 -0
  129. package/src/ui/theme.js +19 -18
  130. package/test/app-building-skills.test.mjs +61 -0
  131. package/test/apps-scaffold-contract.test.mjs +56 -0
  132. package/test/capabilities.test.mjs +53 -4
  133. package/test/cli-preferences.test.mjs +25 -0
  134. package/test/context.test.mjs +95 -0
  135. package/test/indexing-provider.test.mjs +29 -0
  136. package/test/jr-dev-skill.test.mjs +26 -0
  137. package/test/knowledge-context-pack.test.mjs +19 -0
  138. package/test/knowledge.test.mjs +1 -1
  139. package/test/merkle-persistence.test.mjs +91 -0
  140. package/test/merkle.test.mjs +77 -3
  141. package/test/models.test.mjs +37 -0
  142. package/test/ollama.test.mjs +94 -0
  143. package/test/project-config.test.mjs +81 -0
  144. package/test/project-rules.test.mjs +44 -0
  145. package/test/release-hygiene.test.mjs +34 -0
  146. package/test/repository-snapshot-view.test.mjs +112 -0
  147. package/test/runtime-activity.test.mjs +98 -0
  148. package/test/sdk-worker-contract.test.mjs +68 -0
  149. package/test/security.test.mjs +46 -0
  150. package/test/shell.test.mjs +8 -1
  151. package/test/skills.test.mjs +22 -0
  152. package/test/smoke.mjs +2 -2
  153. package/test/theme-portability.test.mjs +14 -0
  154. package/test/tools-search.test.mjs +27 -0
  155. package/examples/agentsam-tui-ansi.mjs +0 -149
  156. package/src/commands/tui.js +0 -120
  157. package/src/ui/splash-xterm.js +0 -290
  158. package/src/ui/splash.js +0 -426
@@ -5,6 +5,7 @@
5
5
  import { execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
7
  import { coreBaseUrl } from './core-client.js';
8
+ import { resolveSdkKey } from '../../packages/identity/src/contracts/auth-config.js';
8
9
 
9
10
  const execFileAsync = promisify(execFile);
10
11
 
@@ -220,22 +221,22 @@ async function detectIam(explicitToken = '') {
220
221
  });
221
222
  }
222
223
 
223
- const sdkToken = explicitToken || process.env.AGENTSAM_SDK_TOKEN || '';
224
+ const sdkToken = resolveSdkKey(process.env, explicitToken);
224
225
  if (sdkToken.trim()) {
225
226
  const probe = await probeSdkBearer(sdkToken);
226
227
  if (probe.valid) {
227
228
  return {
228
- source: 'sdk-token',
229
+ source: 'sdk-key',
229
230
  ready: true,
230
- detail: `AGENTSAM_SDK_TOKEN · user ${probe.user_id || '?'}`,
231
+ detail: `AGENTSAM_SDK_KEY · user ${probe.user_id || '?'}`,
231
232
  probe,
232
233
  aux,
233
234
  };
234
235
  }
235
236
  return {
236
- source: 'sdk-token',
237
+ source: 'sdk-key',
237
238
  ready: false,
238
- detail: `AGENTSAM_SDK_TOKEN invalid (${probe.error}) → will open browser`,
239
+ detail: `AGENTSAM_SDK_KEY invalid (${probe.error}) → will open browser`,
239
240
  probe,
240
241
  aux,
241
242
  };
@@ -75,7 +75,7 @@ migrations_dir = "migrations"
75
75
  files['.env.example'] = `# Minted at install/build — secrets via wrangler secret put (never commit)
76
76
  # IAM_CLIENT_ID= # plaintext var (public by OAuth design)
77
77
  # IAM_CLIENT_SECRET= # wrangler secret put only
78
- # IAM_OAUTH_ISSUER=https://inneranimalmedia.com
78
+ # IAM_ORIGIN=https://inneranimalmedia.com
79
79
 
80
80
  # Developer BYOK — takes /api/oauth/{google|github}/start when set
81
81
  # GOOGLE_CLIENT_ID=
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  import pkg from '../../package.json' with { type: 'json' };
7
+ import { createProjectManifest } from './project-config.js';
8
+ import { defaultProjectRules } from './project-rules.js';
7
9
 
8
10
  const LANE_KEYS = {
9
11
  '1': 'fullstack',
@@ -139,37 +141,24 @@ export function buildLocalScaffoldFiles({
139
141
  const migration = migrationSql(laneKey);
140
142
 
141
143
  return [
142
- {
143
- path: 'agentsam.config.js',
144
- content: `export default {
145
- project: '${projectName}',
146
- lane: '${laneKey}',
147
- agent: '${agent}',
148
- runTarget: '${runTarget}',
149
- api: { baseUrl: '/api/agentsam' },
150
- };
151
- `,
152
- },
153
144
  {
154
145
  path: '.agentsam/config.json',
155
146
  content: `${JSON.stringify(
156
- {
157
- project: projectName,
158
- lane: laneKey,
159
- agent,
160
- run_target: 'local',
161
- deploy_target: runTarget === 'local' ? null : runTarget,
162
- pty_port: 3099,
163
- dev_port: 8787,
164
- db_path: '.agentsam/data/agentsam.sqlite',
165
- db_schema: 'db/schema.sql',
166
- ui: 'terminal',
167
- scaffold_version: sdkVersion,
168
- },
147
+ createProjectManifest({
148
+ projectName,
149
+ preset: laneKey,
150
+ profile: agent,
151
+ runTarget,
152
+ sdkVersion,
153
+ }),
169
154
  null,
170
155
  2,
171
156
  )}\n`,
172
157
  },
158
+ {
159
+ path: '.agentsamrules',
160
+ content: defaultProjectRules(projectName),
161
+ },
173
162
  {
174
163
  path: '.agentsam/start-local.md',
175
164
  content: `# Agent Sam local project
@@ -187,9 +176,10 @@ Useful local surfaces:
187
176
 
188
177
  \`\`\`bash
189
178
  npm run db:status
190
- npm run tui
191
- npm run tui:rich -- --install # optional Rich UI in isolated .agentsam venv
179
+ npx agentsam # enter Agent Sam
192
180
  npx agentsam start-local # PTY on ws://127.0.0.1:3099
181
+ npm run ollama:setup # optional local/free model config
182
+ npm run ollama:status # probe local Ollama + models
193
183
  \`\`\`
194
184
 
195
185
  The local SQLite database lives at \`.agentsam/data/agentsam.sqlite\` and is initialized from \`db/schema.sql\`.
@@ -209,7 +199,12 @@ npx agentsam deploy
209
199
  dist/
210
200
  .wrangler/
211
201
  .agentsam/data/
202
+ .agentsam/cli.json
212
203
  .agentsam/tui-venv/
204
+ .agentsam/knowledge/
205
+ .agentsam/cache/
206
+ .agentsam/merkle/
207
+ .agentsam/merkle.json
213
208
  *.db
214
209
  *.sqlite
215
210
  *.sqlite-*
@@ -231,8 +226,8 @@ dist/
231
226
  'db:init': 'agentsam db init',
232
227
  'db:status': 'agentsam db status',
233
228
  pty: 'agentsam start-local',
234
- tui: 'agentsam tui',
235
- 'tui:rich': 'agentsam tui rich',
229
+ 'ollama:setup': 'agentsam ollama setup',
230
+ 'ollama:status': 'agentsam ollama status',
236
231
  deploy: 'agentsam deploy',
237
232
  },
238
233
  engines: {
@@ -262,6 +257,11 @@ AGENTSAM_LANE=${laneKey}
262
257
  AGENTSAM_AGENT=${agent}
263
258
  AGENTSAM_DB=.agentsam/data/agentsam.sqlite
264
259
  PORT=8787
260
+
261
+ # Optional free/local model runtime. Enable with: agentsam ollama setup
262
+ # OLLAMA_BASE_URL=http://127.0.0.1:11434
263
+ # OLLAMA_MODEL=qwen2.5-coder
264
+ # OLLAMA_EMBED_MODEL=mxbai-embed-large
265
265
  `,
266
266
  },
267
267
  {
@@ -399,13 +399,15 @@ Local state:
399
399
  ## Agent Sam terminal experience
400
400
 
401
401
  \`\`\`bash
402
- npm run tui # zero-dependency ANSI dashboard
403
- npm run tui:rich # Python Rich dashboard if installed
404
- npm run tui:rich -- --install
402
+ npx agentsam # enter the interactive Agent Sam experience
405
403
  npm run pty # local PTY on ws://127.0.0.1:3099
406
404
  npm run db:status
405
+ npm run ollama:setup # optional local/free model config
406
+ npm run ollama:status # local Ollama + model readiness
407
407
  \`\`\`
408
408
 
409
+ Ollama is optional and local-only; it is not deployed as an edge model. Remote AgentSam sessions can reach it by executing the CLI through the existing user-hosted terminal tunnel.
410
+
409
411
  No Worker or cloud account is required for local development.
410
412
 
411
413
  ## Graduate intentionally
@@ -443,9 +445,9 @@ export function buildLocalScaffoldMeta(body, sdkVersion = pkg.version) {
443
445
  'npm run smoke',
444
446
  'npm run status',
445
447
  'npm run dev',
446
- 'npm run tui',
448
+ 'npx agentsam',
447
449
  'npm run db:status',
448
- 'Optional: npm run tui:rich -- --install',
450
+ 'Optional free/local models: npm run ollama:setup',
449
451
  'Optional: npm run pty',
450
452
  'When ready for cloud: npm run deploy',
451
453
  ],
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { tryResolveGitContext } from './git-context.js';
4
4
  import { inspectLocalSqlite } from '../local/sqlite.js';
5
+ import { getCreatedWithVersion, getDefaultProfile, getDeployTarget, getLocalDatabasePath, getProjectName, getProjectPreset, getRepositoryId, tryReadProjectConfig } from './project-config.js';
5
6
 
6
7
  export function findAgentSamProjectRoot(startDir = process.cwd()) {
7
8
  let dir = path.resolve(startDir);
@@ -14,16 +15,6 @@ export function findAgentSamProjectRoot(startDir = process.cwd()) {
14
15
  return path.resolve(startDir);
15
16
  }
16
17
 
17
- function readProjectConfig(root) {
18
- const configPath = path.join(root, '.agentsam', 'config.json');
19
- if (!fs.existsSync(configPath)) return null;
20
- try {
21
- return JSON.parse(fs.readFileSync(configPath, 'utf8'));
22
- } catch {
23
- return null;
24
- }
25
- }
26
-
27
18
  async function probe(url) {
28
19
  try {
29
20
  const response = await fetch(url, { signal: AbortSignal.timeout(500) });
@@ -35,12 +26,12 @@ async function probe(url) {
35
26
 
36
27
  export async function collectLocalStatus(cwd = process.cwd()) {
37
28
  const root = findAgentSamProjectRoot(cwd);
38
- const config = readProjectConfig(root);
29
+ const config = tryReadProjectConfig(root);
39
30
  const git = tryResolveGitContext({ cwd: root });
40
31
 
41
32
  let db = { ok: false, exists: false, tables: [] };
42
33
  if (config) {
43
- db = await inspectLocalSqlite(path.join(root, config.db_path || '.agentsam/data/agentsam.sqlite'));
34
+ db = await inspectLocalSqlite(path.join(root, getLocalDatabasePath(config)));
44
35
  }
45
36
 
46
37
  const devPort = config?.dev_port ?? 8787;
@@ -54,11 +45,12 @@ export async function collectLocalStatus(cwd = process.cwd()) {
54
45
  schemaVersion: 'agentsam-local-status-v1',
55
46
  configured: Boolean(config),
56
47
  root,
57
- project: config?.project || path.basename(root),
58
- lane: config?.lane || null,
59
- agent: config?.agent || null,
60
- deployTarget: config?.deploy_target || null,
61
- scaffoldVersion: config?.scaffold_version || null,
48
+ project: getProjectName(config, path.basename(root)),
49
+ repositoryId: getRepositoryId(config),
50
+ lane: getProjectPreset(config),
51
+ agent: getDefaultProfile(config),
52
+ deployTarget: getDeployTarget(config),
53
+ scaffoldVersion: getCreatedWithVersion(config),
62
54
  git: git
63
55
  ? {
64
56
  repo: git.repoFullName || null,
@@ -0,0 +1,321 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { execFileSync } from 'node:child_process';
6
+ import { resolveGitContext } from '../git-context.js';
7
+ import {
8
+ MERKLE_SNAPSHOT_STORAGE_PREFIX,
9
+ MERKLE_SNAPSHOT_TABLE,
10
+ MERKLE_WEBSITE_ASSETS_BINDING,
11
+ merkleSnapshotStorageKey,
12
+ normalizeMerkleStoragePrefix,
13
+ } from './persistence.js';
14
+
15
+ const D1_BINDING = 'DB';
16
+ const CAPTURE_KINDS = new Set(['deploy', 'manual', 'agent', 'index']);
17
+ const SOURCES = new Set(['github', 'gitlab', 'bitbucket', 'local', 'upload']);
18
+
19
+ function clean(value) {
20
+ return value == null ? '' : String(value).trim();
21
+ }
22
+
23
+ function stripJsonComments(input) {
24
+ let out = '';
25
+ let string = false;
26
+ let quote = '';
27
+ let escaped = false;
28
+ let line = false;
29
+ let block = false;
30
+ for (let i = 0; i < input.length; i += 1) {
31
+ const char = input[i];
32
+ const next = input[i + 1];
33
+ if (line) {
34
+ if (char === '\n') { line = false; out += char; }
35
+ continue;
36
+ }
37
+ if (block) {
38
+ if (char === '*' && next === '/') { block = false; i += 1; }
39
+ continue;
40
+ }
41
+ if (string) {
42
+ out += char;
43
+ if (escaped) escaped = false;
44
+ else if (char === '\\') escaped = true;
45
+ else if (char === quote) string = false;
46
+ continue;
47
+ }
48
+ if (char === '"' || char === "'") { string = true; quote = char; out += char; continue; }
49
+ if (char === '/' && next === '/') { line = true; i += 1; continue; }
50
+ if (char === '/' && next === '*') { block = true; i += 1; continue; }
51
+ out += char;
52
+ }
53
+ return out;
54
+ }
55
+
56
+ function parseTomlBindings(text) {
57
+ const rows = { r2_buckets: [], d1_databases: [] };
58
+ let current = null;
59
+ let currentType = null;
60
+ const flush = () => {
61
+ if (current && currentType) rows[currentType].push(current);
62
+ current = null;
63
+ currentType = null;
64
+ };
65
+ for (const raw of text.split(/\r?\n/)) {
66
+ const line = raw.trim();
67
+ const header = line.match(/^\[\[(?:env\.([^.\]]+)\.)?(r2_buckets|d1_databases)\]\]$/);
68
+ if (header) {
69
+ flush();
70
+ currentType = header[2];
71
+ current = { environment: header[1] || null };
72
+ continue;
73
+ }
74
+ if (/^\[/.test(line)) { flush(); continue; }
75
+ if (!current || !line || line.startsWith('#')) continue;
76
+ const match = line.match(/^([A-Za-z0-9_]+)\s*=\s*["']([^"']*)["']/);
77
+ if (match) current[match[1]] = match[2];
78
+ }
79
+ flush();
80
+ return rows;
81
+ }
82
+
83
+ function rowsFromJson(parsed, environment) {
84
+ const base = parsed && typeof parsed === 'object' ? parsed : {};
85
+ const env = environment && base.env?.[environment] && typeof base.env[environment] === 'object'
86
+ ? base.env[environment]
87
+ : {};
88
+ return {
89
+ r2_buckets: [...(env.r2_buckets || []), ...(base.r2_buckets || [])],
90
+ d1_databases: [...(env.d1_databases || []), ...(base.d1_databases || [])],
91
+ };
92
+ }
93
+
94
+ export function readWranglerPersistenceBindings(configPath, { environment = null } = {}) {
95
+ const filename = path.resolve(configPath);
96
+ const text = fs.readFileSync(filename, 'utf8');
97
+ const ext = path.extname(filename).toLowerCase();
98
+ let rows;
99
+ if (ext === '.json' || ext === '.jsonc') rows = rowsFromJson(JSON.parse(stripJsonComments(text)), environment);
100
+ else rows = parseTomlBindings(text);
101
+ const scoped = (items) => {
102
+ if (!environment) return items.filter((row) => !row.environment);
103
+ const envRows = items.filter((row) => row.environment === environment);
104
+ return envRows.length ? envRows : items.filter((row) => !row.environment);
105
+ };
106
+ return { configPath: filename, r2_buckets: scoped(rows.r2_buckets), d1_databases: scoped(rows.d1_databases) };
107
+ }
108
+
109
+ export function resolveWranglerMerklePersistence({
110
+ configPath,
111
+ environment = null,
112
+ r2Binding = MERKLE_WEBSITE_ASSETS_BINDING,
113
+ d1Binding = D1_BINDING,
114
+ r2Only = false,
115
+ } = {}) {
116
+ if (!clean(configPath)) throw new Error('wrangler_config_required');
117
+ const rows = readWranglerPersistenceBindings(configPath, { environment });
118
+ const r2 = rows.r2_buckets.find((row) => clean(row.binding) === clean(r2Binding));
119
+ if (!r2?.bucket_name) throw new Error(`wrangler_r2_binding_not_found:${r2Binding}`);
120
+ const d1 = rows.d1_databases.find((row) => clean(row.binding) === clean(d1Binding));
121
+ if (!r2Only && !d1?.database_name) throw new Error(`wrangler_d1_binding_not_found:${d1Binding}`);
122
+ return {
123
+ config_path: rows.configPath,
124
+ environment: environment || null,
125
+ r2_binding: r2Binding,
126
+ storage_bucket: String(r2.bucket_name),
127
+ d1_binding: r2Only ? null : d1Binding,
128
+ database_name: r2Only ? null : String(d1.database_name),
129
+ };
130
+ }
131
+
132
+ function providerRepoId(git) {
133
+ const host = clean(git?.remoteHost).toLowerCase();
134
+ const fullName = clean(git?.repoFullName);
135
+ if (!fullName) return null;
136
+ if (host === 'github.com') return `github:${fullName}`;
137
+ if (host === 'gitlab.com') return `gitlab:${fullName}`;
138
+ if (host === 'bitbucket.org') return `bitbucket:${fullName}`;
139
+ return null;
140
+ }
141
+
142
+ function safeId(value) {
143
+ return clean(value).replace(/[^A-Za-z0-9._-]+/g, '').slice(0, 96);
144
+ }
145
+
146
+ function snapshotIdFor({ snapshot, repoId, captureKind, deploymentId }) {
147
+ const deployment = safeId(deploymentId);
148
+ if (captureKind === 'deploy' && deployment) return `mrs_dep_${deployment.toLowerCase()}`;
149
+ const digest = createHash('sha256')
150
+ .update([repoId, snapshot.rootHash, snapshot.semantic?.rootHash || '', captureKind].join('\0'))
151
+ .digest('hex');
152
+ return `mrs_${digest.slice(0, 24)}`;
153
+ }
154
+
155
+ function sqlEsc(value) {
156
+ return String(value ?? '').replaceAll("'", "''");
157
+ }
158
+
159
+ function sqlText(value) {
160
+ return value == null || value === '' ? 'NULL' : `'${sqlEsc(value)}'`;
161
+ }
162
+
163
+ function sqlInt(value) {
164
+ return value == null || value === '' || !Number.isFinite(Number(value)) ? 'NULL' : String(Math.trunc(Number(value)));
165
+ }
166
+
167
+ export function buildMerklePersistencePlan({
168
+ snapshot,
169
+ root = process.cwd(),
170
+ ownerUserId,
171
+ repoId,
172
+ source = 'local',
173
+ captureKind = 'manual',
174
+ connectionId = null,
175
+ runtimeLeaseId = null,
176
+ deploymentId = null,
177
+ workerVersionId = null,
178
+ referenceLabel = null,
179
+ storagePrefix = MERKLE_SNAPSHOT_STORAGE_PREFIX,
180
+ wrangler,
181
+ } = {}) {
182
+ if (!snapshot?.rootHash || !Array.isArray(snapshot?.entries)) throw new Error('merkle_snapshot_required');
183
+ const owner = clean(ownerUserId);
184
+ if (!owner) throw new Error('owner_user_id_required');
185
+ if (!CAPTURE_KINDS.has(captureKind)) throw new Error(`capture_kind_invalid:${captureKind}`);
186
+ if (!SOURCES.has(source)) throw new Error(`source_invalid:${source}`);
187
+ if (captureKind !== 'deploy' && !clean(connectionId) && !clean(runtimeLeaseId)) throw new Error('execution_provenance_required');
188
+ let git = null;
189
+ try { git = resolveGitContext({ cwd: root }); } catch { git = null; }
190
+ const resolvedRepoId = clean(repoId) || providerRepoId(git);
191
+ if (!resolvedRepoId) throw new Error('repo_id_required');
192
+ const snapshotId = snapshotIdFor({ snapshot, repoId: resolvedRepoId, captureKind, deploymentId });
193
+ const prefix = normalizeMerkleStoragePrefix(storagePrefix);
194
+ const storageKey = merkleSnapshotStorageKey({ ownerUserId: owner, repoId: resolvedRepoId, snapshotId, prefix });
195
+ const createdAt = Math.floor(Date.now() / 1000);
196
+ const classifier = snapshot.semantic?.classifier || null;
197
+ const row = {
198
+ snapshot_id: snapshotId,
199
+ owner_user_id: owner,
200
+ repo_id: resolvedRepoId,
201
+ repository: git?.remoteUrl || null,
202
+ source,
203
+ manifest_format: snapshot.format || 'agentsam-merkle',
204
+ manifest_version: snapshot.version || 1,
205
+ hash_algorithm: snapshot.algorithm || 'sha256',
206
+ root_hash: snapshot.rootHash,
207
+ policy_hash: snapshot.policyHash || null,
208
+ resolved_commit_sha: git?.revisionSha || null,
209
+ resolved_tree_sha: null,
210
+ git_branch: git?.branch || null,
211
+ working_tree_dirty: git?.dirty ? 1 : 0,
212
+ connection_id: clean(connectionId) || null,
213
+ runtime_lease_id: clean(runtimeLeaseId) || null,
214
+ storage_backend: 'r2',
215
+ storage_bucket: wrangler.storage_bucket,
216
+ storage_key: storageKey,
217
+ entry_count: snapshot.entries.length,
218
+ file_count: snapshot.stats?.files ?? null,
219
+ directory_count: snapshot.stats?.directories ?? null,
220
+ symlink_count: snapshot.stats?.symlinks ?? 0,
221
+ total_bytes: snapshot.stats?.bytes ?? null,
222
+ capture_kind: captureKind,
223
+ deployment_id: clean(deploymentId) || null,
224
+ worker_version_id: clean(workerVersionId) || null,
225
+ reference_label: clean(referenceLabel) || `${captureKind}:${snapshotId}`,
226
+ created_at: createdAt,
227
+ persisted_at: createdAt,
228
+ metadata_root: snapshot.semantic?.rootHash || null,
229
+ classifier_format: classifier?.format || null,
230
+ classifier_version: classifier?.version ?? null,
231
+ classifier_source: classifier?.source || null,
232
+ };
233
+ return { snapshot_id: snapshotId, storage_key: storageKey, row, wrangler, prefix };
234
+ }
235
+
236
+ export function merklePersistenceUpsertSql(row) {
237
+ return `INSERT INTO ${MERKLE_SNAPSHOT_TABLE} (
238
+ snapshot_id, owner_user_id, repo_id, repository, source,
239
+ manifest_format, manifest_version, hash_algorithm, root_hash, policy_hash,
240
+ resolved_commit_sha, resolved_tree_sha, git_branch, working_tree_dirty,
241
+ connection_id, runtime_lease_id, storage_backend, storage_bucket, storage_key,
242
+ entry_count, file_count, directory_count, symlink_count, total_bytes,
243
+ capture_kind, deployment_id, worker_version_id, reference_label,
244
+ created_at, persisted_at, metadata_root, classifier_format, classifier_version, classifier_source
245
+ ) VALUES (
246
+ ${sqlText(row.snapshot_id)}, ${sqlText(row.owner_user_id)}, ${sqlText(row.repo_id)}, ${sqlText(row.repository)}, ${sqlText(row.source)},
247
+ ${sqlText(row.manifest_format)}, ${sqlInt(row.manifest_version)}, ${sqlText(row.hash_algorithm)}, ${sqlText(row.root_hash)}, ${sqlText(row.policy_hash)},
248
+ ${sqlText(row.resolved_commit_sha)}, ${sqlText(row.resolved_tree_sha)}, ${sqlText(row.git_branch)}, ${sqlInt(row.working_tree_dirty)},
249
+ ${sqlText(row.connection_id)}, ${sqlText(row.runtime_lease_id)}, ${sqlText(row.storage_backend)}, ${sqlText(row.storage_bucket)}, ${sqlText(row.storage_key)},
250
+ ${sqlInt(row.entry_count)}, ${sqlInt(row.file_count)}, ${sqlInt(row.directory_count)}, ${sqlInt(row.symlink_count)}, ${sqlInt(row.total_bytes)},
251
+ ${sqlText(row.capture_kind)}, ${sqlText(row.deployment_id)}, ${sqlText(row.worker_version_id)}, ${sqlText(row.reference_label)},
252
+ ${sqlInt(row.created_at)}, ${sqlInt(row.persisted_at)}, ${sqlText(row.metadata_root)}, ${sqlText(row.classifier_format)}, ${sqlInt(row.classifier_version)}, ${sqlText(row.classifier_source)}
253
+ )
254
+ ON CONFLICT(snapshot_id) DO UPDATE SET
255
+ storage_backend=excluded.storage_backend,
256
+ storage_bucket=excluded.storage_bucket,
257
+ storage_key=excluded.storage_key,
258
+ persisted_at=excluded.persisted_at,
259
+ deployment_id=excluded.deployment_id,
260
+ worker_version_id=excluded.worker_version_id,
261
+ reference_label=excluded.reference_label,
262
+ metadata_root=excluded.metadata_root,
263
+ classifier_format=excluded.classifier_format,
264
+ classifier_version=excluded.classifier_version,
265
+ classifier_source=excluded.classifier_source;`;
266
+ }
267
+
268
+ function wranglerArgs(base, wrangler) {
269
+ const args = [...base, '--config', wrangler.config_path];
270
+ if (wrangler.environment) args.push('--env', wrangler.environment);
271
+ return args;
272
+ }
273
+
274
+ export function persistMerkleSnapshotCloudflare({
275
+ snapshotPath,
276
+ snapshot,
277
+ plan,
278
+ wranglerBin = process.env.WRANGLER_BIN || 'wrangler',
279
+ r2Only = false,
280
+ dryRun = false,
281
+ } = {}) {
282
+ const filename = path.resolve(snapshotPath);
283
+ if (!fs.existsSync(filename)) throw new Error('snapshot_file_not_found');
284
+ const result = {
285
+ provider: 'cloudflare',
286
+ binding: plan.wrangler.r2_binding,
287
+ storage_bucket: plan.row.storage_bucket,
288
+ storage_key: plan.row.storage_key,
289
+ database_binding: plan.wrangler.d1_binding,
290
+ database_name: plan.wrangler.database_name,
291
+ table: r2Only ? null : MERKLE_SNAPSHOT_TABLE,
292
+ snapshot_id: plan.row.snapshot_id,
293
+ row: plan.row,
294
+ dry_run: Boolean(dryRun),
295
+ };
296
+ if (dryRun) return result;
297
+
298
+ const object = `${plan.row.storage_bucket}/${plan.row.storage_key}`;
299
+ execFileSync(wranglerBin, wranglerArgs(['r2', 'object', 'put', object, '--file', filename, '--remote'], plan.wrangler), {
300
+ stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8',
301
+ });
302
+ if (r2Only) return result;
303
+
304
+ const tmp = path.join(os.tmpdir(), `agentsam-merkle-persist-${process.pid}-${Date.now()}.sql`);
305
+ fs.writeFileSync(tmp, `${merklePersistenceUpsertSql(plan.row)}\n`, { encoding: 'utf8', mode: 0o600 });
306
+ try {
307
+ execFileSync(wranglerBin, wranglerArgs(['d1', 'execute', plan.wrangler.database_name, '--remote', '--yes', `--file=${tmp}`], plan.wrangler), {
308
+ stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8',
309
+ });
310
+ } catch (error) {
311
+ try {
312
+ execFileSync(wranglerBin, wranglerArgs(['r2', 'object', 'delete', object, '--remote'], plan.wrangler), {
313
+ stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8',
314
+ });
315
+ } catch {}
316
+ throw error;
317
+ } finally {
318
+ fs.rmSync(tmp, { force: true });
319
+ }
320
+ return result;
321
+ }
@@ -0,0 +1,43 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { comparePaths } from './hash.js';
3
+
4
+ export const FILEMETA_FORMAT = 'agentsam-filemeta';
5
+ export const FILEMETA_VERSION = 1;
6
+
7
+ function canonical(value) {
8
+ if (Array.isArray(value)) return value.map(canonical);
9
+ if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
10
+ return value;
11
+ }
12
+
13
+ function semanticDigest(type, value) {
14
+ return 'sha256:' + createHash('sha256')
15
+ .update(`agentsam-filemeta:${type}:v1\0`)
16
+ .update(JSON.stringify(canonical(value)))
17
+ .digest('hex');
18
+ }
19
+
20
+ export function metadataRoot(entries, classifier) {
21
+ return semanticDigest('root', { classifier, entries: [...entries].sort((a, b) => comparePaths(a.path, b.path)) });
22
+ }
23
+
24
+ export function validateSemanticMetadata(value, contentEntries = []) {
25
+ const fail = (message) => { throw new Error(`Invalid semantic metadata: ${message}`); };
26
+ if (!value || value.format !== FILEMETA_FORMAT || value.version !== FILEMETA_VERSION || value.algorithm !== 'sha256') fail('unsupported format/version');
27
+ if (!value.classifier || !Array.isArray(value.entries)) fail('missing classifier/entries');
28
+ const content = new Map(contentEntries.filter((entry) => entry.type !== 'directory').map((entry) => [entry.path, entry]));
29
+ const seen = new Set();
30
+ for (const entry of value.entries) {
31
+ if (!entry || typeof entry.path !== 'string' || seen.has(entry.path)) fail('invalid/duplicate path');
32
+ seen.add(entry.path);
33
+ if (!Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o7777) fail(`invalid mode: ${entry.path}`);
34
+ if (!/^sha256:[a-f0-9]{64}$/.test(entry.hash || '')) fail(`invalid hash: ${entry.path}`);
35
+ const source = content.get(entry.path);
36
+ if (source && source.hash !== entry.hash) fail(`content hash mismatch: ${entry.path}`);
37
+ if (source?.type === 'file' && source.size !== entry.size) fail(`file size mismatch: ${entry.path}`);
38
+ }
39
+ if (content.size && seen.size !== content.size) fail('entry count does not match content tree');
40
+ const rootHash = metadataRoot(value.entries, value.classifier);
41
+ if (rootHash !== value.rootHash) fail('metadata root mismatch');
42
+ return { ...value, rootHash, entries: [...value.entries].sort((a, b) => comparePaths(a.path, b.path)) };
43
+ }
@@ -0,0 +1,24 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ const execute = promisify(execFile);
5
+
6
+ /**
7
+ * Resolve Git-ignored paths using Git itself so every Merkle-backed consumer can
8
+ * build evidence from the same checkout policy. Non-Git directories simply
9
+ * return an empty list and still receive the protocol's built-in excludes.
10
+ */
11
+ export async function gitIgnoredPaths(root) {
12
+ try {
13
+ const result = await execute('git', ['status', '--porcelain=v1', '--ignored=matching'], {
14
+ cwd: root,
15
+ maxBuffer: 16 * 1024 * 1024,
16
+ });
17
+ return [...new Set(result.stdout.split('\n')
18
+ .filter((line) => line.startsWith('!! '))
19
+ .map((line) => line.slice(3).trim().replace(/\/$/, ''))
20
+ .filter((value) => value && !value.includes('\\') && !value.split('/').includes('..')))];
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
@@ -4,6 +4,7 @@ export const comparePaths = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from
4
4
  export const digest = (type, value) => 'sha256:' + createHash('sha256')
5
5
  .update(`agentsam-merkle:${type}:v1\0`).update(value).digest('hex');
6
6
  export const linkHash = (target) => digest('symlink', target);
7
+ export const policyHash = (policy) => digest('policy', JSON.stringify(policy));
7
8
  export const directoryHash = (children) => digest('directory', JSON.stringify(
8
9
  [...children].sort((a, b) => comparePaths(a.name, b.name)).map(({ name, type, hash }) => [name, type, hash]),
9
10
  ));
@@ -2,3 +2,25 @@ export { buildMerkleTree } from './tree.js';
2
2
  export { saveSnapshot, readSnapshot, validateSnapshot } from './snapshot.js';
3
3
  export { diffTrees } from './diff.js';
4
4
  export { DEFAULT_IGNORES, normalizePolicy } from './policy.js';
5
+ export { policyHash } from './hash.js';
6
+ export async function buildSemanticMetadata(...args) {
7
+ const semantic = await import('./semantic.js');
8
+ return semantic.buildSemanticMetadata(...args);
9
+ }
10
+ export { validateSemanticMetadata, metadataRoot, FILEMETA_FORMAT, FILEMETA_VERSION } from './filemeta.js';
11
+
12
+ export {
13
+ MERKLE_SNAPSHOT_SCHEMA_SQL,
14
+ MERKLE_SNAPSHOT_STORAGE_PREFIX,
15
+ MERKLE_SNAPSHOT_TABLE,
16
+ MERKLE_WEBSITE_ASSETS_BINDING,
17
+ merkleSnapshotStorageKey,
18
+ normalizeMerkleStoragePrefix,
19
+ } from './persistence.js';
20
+ export {
21
+ buildMerklePersistencePlan,
22
+ merklePersistenceUpsertSql,
23
+ persistMerkleSnapshotCloudflare,
24
+ readWranglerPersistenceBindings,
25
+ resolveWranglerMerklePersistence,
26
+ } from './cloudflare-persistence.js';