@inneranimalmedia/agentsam-sdk 2.4.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTSAM.md +55 -0
- package/README.md +17 -12
- package/bin/agentsam +2 -0
- package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
- package/docs/AGENTSAM_WORKBENCH.md +30 -0
- package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
- package/docs/CAPABILITIES.md +5 -3
- package/docs/CLI_SHELL.md +167 -80
- package/docs/CMS_STUDIO.md +101 -0
- package/docs/CONTEXT.md +170 -0
- package/docs/LOCAL_OLLAMA.md +58 -0
- package/docs/MERKLE.md +19 -19
- package/docs/PORTABLE_CONTEXT.md +4 -3
- package/docs/PROJECT_CONFIG.md +72 -0
- package/docs/RELEASES.md +35 -2
- package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
- package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
- package/docs/SDK_WORKER.md +86 -0
- package/docs/SECURITY.md +59 -22
- package/docs/client-cms-editor.md +15 -0
- package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
- package/package.json +35 -10
- package/packages/connectors/cloudflare/package.json +10 -0
- package/packages/connectors/cloudflare/src/index.js +127 -0
- package/packages/connectors/cloudflare/src/owner.js +76 -0
- package/packages/connectors/cloudflare/src/routes.js +223 -0
- package/packages/connectors/cloudflare/src/vault.js +80 -0
- package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
- package/packages/identity/package.json +10 -1
- package/packages/identity/src/contracts/auth-config.js +109 -0
- package/packages/identity/src/index.js +1 -0
- package/packages/identity/src/oauth/README.md +2 -2
- package/packages/identity/src/oauth/credentials.js +11 -4
- package/packages/identity/src/oauth/iam-platform.js +3 -3
- package/packages/identity/src/providers/iam/index.js +7 -7
- package/packages/identity/src/providers/iam/oauth.js +6 -4
- package/packages/identity/src/providers/iam/profile.js +5 -5
- package/packages/identity/tests/auth-config.test.mjs +61 -0
- package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
- package/protocol/FILEMETA_V1.md +95 -0
- package/protocol/INSPECT_VIEWS_V1.md +40 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
- package/protocol/MERKLE_V1.md +3 -1
- package/protocol/README.md +1 -0
- package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
- package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
- package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
- package/protocol/capabilities/manifest.json +79 -3
- package/protocol/capabilities/repository-snapshot.schema.json +1 -0
- package/protocol/context/context-budget.schema.json +36 -0
- package/protocol/context/context-item.schema.json +19 -0
- package/protocol/context/resolved-context-pack.schema.json +47 -0
- package/protocol/context/result-policy.schema.json +17 -0
- package/protocol/knowledge/context-pack.schema.json +33 -0
- package/protocol/knowledge/retrieval-query.schema.json +68 -13
- package/protocol/models/README.md +373 -0
- package/protocol/models/model-inventory-v2.schema.json +212 -0
- package/python/README.md +15 -7
- package/python/agentsam_sdk/cli.py +0 -21
- package/python/agentsam_sdk/tui/README.md +17 -12
- package/python/agentsam_sdk/tui/bootstrap.py +2 -2
- package/python/agentsam_sdk/tui/demo.py +25 -10
- package/python/agentsam_sdk/tui/onboarding.py +208 -0
- package/python/tests/test_tui_cli.py +7 -6
- package/skills/README.md +21 -0
- package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
- package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
- package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
- package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
- package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
- package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
- package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
- package/skills/agentsam-jr-dev/SKILL.md +232 -0
- package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
- package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
- package/skills/agentsam-progression-guard/SKILL.md +197 -0
- package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
- package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
- package/skills/catalog.json +71 -0
- package/src/agent/capability-adapter.js +25 -13
- package/src/agent/index.js +1 -0
- package/src/agent/responses-runner.js +325 -0
- package/src/capabilities/index.js +7 -0
- package/src/capabilities/repository-snapshot-view.js +238 -0
- package/src/capabilities/repository-snapshot.js +28 -14
- package/src/cli.js +198 -76
- package/src/cloudflare/cpu-profile.js +115 -0
- package/src/cloudflare/index.js +14 -0
- package/src/cloudflare/wrangler.js +132 -0
- package/src/commands/account-auth.js +47 -0
- package/src/commands/cloudflare.js +58 -0
- package/src/commands/connections.js +93 -0
- package/src/commands/context-economics.js +114 -0
- package/src/commands/context.js +14 -2
- package/src/commands/db.js +4 -7
- package/src/commands/deploy.js +54 -32
- package/src/commands/eval.js +63 -0
- package/src/commands/interactive.js +18 -0
- package/src/commands/knowledge.js +27 -4
- package/src/commands/merkle-persist.js +118 -0
- package/src/commands/merkle.js +32 -17
- package/src/commands/models.js +152 -0
- package/src/commands/ollama.js +259 -0
- package/src/commands/preferences.js +144 -0
- package/src/commands/product.js +86 -16
- package/src/commands/resume.js +67 -0
- package/src/commands/security.js +8 -6
- package/src/commands/shell.js +405 -100
- package/src/commands/skills.js +66 -0
- package/src/commands/start-local.js +1 -1
- package/src/commands/tunnel.js +5 -4
- package/src/commands/whoami.js +86 -0
- package/src/context/budget.js +120 -0
- package/src/context/compact.js +28 -0
- package/src/context/index.js +7 -0
- package/src/context/rehydrate.js +35 -0
- package/src/context/resolve.js +116 -0
- package/src/context/result-policy.js +66 -0
- package/src/errors/diagnostic.js +160 -0
- package/src/errors/index.js +9 -0
- package/src/eval/context.js +191 -0
- package/src/eval/index.js +1 -0
- package/src/index.js +70 -0
- package/src/indexing/execution-boundary.js +144 -0
- package/src/indexing/index.js +8 -0
- package/src/indexing/provider.js +41 -0
- package/src/knowledge/config.js +2 -2
- package/src/knowledge/context-pack.js +12 -1
- package/src/knowledge/contracts.js +9 -4
- package/src/knowledge/engine.js +7 -3
- package/src/knowledge/service/server.js +1 -1
- package/src/lib/account-session.js +98 -0
- package/src/lib/agent-instructions.js +73 -0
- package/src/lib/auth.js +4 -0
- package/src/lib/bridge-client.js +7 -5
- package/src/lib/cli-preferences.js +78 -0
- package/src/lib/core-client.js +8 -8
- package/src/lib/deploy/git-guard.js +69 -0
- package/src/lib/deploy/health.js +57 -0
- package/src/lib/deploy/local-studio.js +283 -0
- package/src/lib/deploy/secret-scan.js +65 -0
- package/src/lib/deploy-receipt/index.js +5 -2
- package/src/lib/detect-context.js +6 -5
- package/src/lib/execution-approvals.js +59 -0
- package/src/lib/identity-scaffold.js +1 -1
- package/src/lib/local-scaffold.js +35 -33
- package/src/lib/local-sessions.js +127 -0
- package/src/lib/local-status.js +9 -17
- package/src/lib/merkle/cloudflare-persistence.js +321 -0
- package/src/lib/merkle/filemeta.js +43 -0
- package/src/lib/merkle/git-ignore.js +24 -0
- package/src/lib/merkle/hash.js +1 -0
- package/src/lib/merkle/index.js +22 -0
- package/src/lib/merkle/persistence.js +72 -0
- package/src/lib/merkle/semantic.js +359 -0
- package/src/lib/merkle/snapshot.js +9 -3
- package/src/lib/merkle/tree.js +11 -6
- package/src/lib/project-config.js +227 -0
- package/src/lib/project-rules.js +68 -0
- package/src/lib/provider-credentials.js +83 -0
- package/src/lib/save-sdk-token.js +1 -1
- package/src/lib/scaffold/templates/worker-api/index.js +101 -20
- package/src/lib/scaffold/wizards/worker-api.js +27 -11
- package/src/lib/slash-commands.js +22 -15
- package/src/lib/tools.js +11 -5
- package/src/models/catalog.js +135 -0
- package/src/models/index.js +7 -0
- package/src/providers/index.js +5 -0
- package/src/providers/openai-responses.js +275 -0
- package/src/security/index.js +1 -0
- package/src/security/inventory.js +4 -1
- package/src/security/process.js +35 -9
- package/src/security/render.js +27 -5
- package/src/security/scan.js +24 -9
- package/src/security/trust-boundary.js +24 -0
- package/src/skills/index.js +64 -0
- package/src/telemetry/contracts.js +203 -0
- package/src/telemetry/events.js +48 -0
- package/src/telemetry/index.js +8 -0
- package/src/tools/hydrate.js +35 -0
- package/src/tools/index.js +2 -0
- package/src/tools/search.js +70 -0
- package/src/ui/ansi.js +1 -1
- package/src/ui/boot.js +54 -0
- package/src/ui/merkle/render.js +1 -0
- package/src/ui/runtime-activity.js +192 -0
- package/src/ui/theme.js +19 -18
- package/test/account-session.test.mjs +36 -0
- package/test/app-building-skills.test.mjs +61 -0
- package/test/apps-scaffold-contract.test.mjs +56 -0
- package/test/capabilities.test.mjs +53 -4
- package/test/cli-preferences.test.mjs +46 -0
- package/test/cloudflare-connector.test.mjs +96 -0
- package/test/cloudflare-runtime.test.mjs +75 -0
- package/test/context.test.mjs +144 -0
- package/test/deploy-health-scan.test.mjs +67 -0
- package/test/error-diagnostics.test.mjs +59 -0
- package/test/eval-context.test.mjs +37 -0
- package/test/execution-approvals.test.mjs +27 -0
- package/test/indexing-provider.test.mjs +29 -0
- package/test/jr-dev-skill.test.mjs +26 -0
- package/test/knowledge-context-pack.test.mjs +19 -0
- package/test/knowledge.test.mjs +1 -1
- package/test/local-sessions.test.mjs +42 -0
- package/test/local-studio-deploy.test.mjs +83 -0
- package/test/merkle-persistence.test.mjs +91 -0
- package/test/merkle.test.mjs +77 -3
- package/test/model-catalog.test.mjs +43 -0
- package/test/models.test.mjs +51 -0
- package/test/npm10-lock.test.mjs +29 -0
- package/test/ollama.test.mjs +94 -0
- package/test/openai-responses.test.mjs +95 -0
- package/test/project-config.test.mjs +81 -0
- package/test/project-rules.test.mjs +44 -0
- package/test/provider-credentials.test.mjs +52 -0
- package/test/rehydrate.test.mjs +25 -0
- package/test/release-hygiene.test.mjs +34 -0
- package/test/repository-snapshot-view.test.mjs +112 -0
- package/test/responses-runner.test.mjs +148 -0
- package/test/runtime-activity.test.mjs +98 -0
- package/test/sdk-worker-contract.test.mjs +68 -0
- package/test/security.test.mjs +46 -0
- package/test/shell.test.mjs +55 -21
- package/test/skills.test.mjs +22 -0
- package/test/smoke.mjs +5 -2
- package/test/telemetry.test.mjs +79 -0
- package/test/theme-portability.test.mjs +14 -0
- package/test/tools-search.test.mjs +40 -0
- package/test/whoami-resume.test.mjs +56 -0
- package/examples/agentsam-tui-ansi.mjs +0 -149
- package/src/commands/tui.js +0 -120
- package/src/ui/splash-xterm.js +0 -290
- package/src/ui/splash.js +0 -426
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
export const LOCAL_SESSION_SCHEMA = 'agentsam-local-session-v1';
|
|
7
|
+
|
|
8
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
9
|
+
function now() { return new Date().toISOString(); }
|
|
10
|
+
|
|
11
|
+
export function localSessionDirectory(options = {}) {
|
|
12
|
+
const home = path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir());
|
|
13
|
+
return path.join(home, '.agentsam', 'sessions');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createLocalSessionId() {
|
|
17
|
+
return `asess_${randomUUID()}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sessionTitleFromInput(input, maxChars = 72) {
|
|
21
|
+
const compact = clean(input).replace(/\s+/g, ' ');
|
|
22
|
+
if (!compact) return 'New Agent Sam session';
|
|
23
|
+
return compact.length <= maxChars ? compact : `${compact.slice(0, Math.max(1, maxChars - 1)).trimEnd()}…`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateSessionId(sessionId) {
|
|
27
|
+
const id = clean(sessionId);
|
|
28
|
+
if (!/^asess_[0-9a-f-]{36}$/i.test(id)) throw new Error(`invalid_session_id:${id || 'empty'}`);
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function filenameFor(sessionId, options = {}) {
|
|
33
|
+
return path.join(localSessionDirectory(options), `${validateSessionId(sessionId)}.json`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureDirectory(options = {}) {
|
|
37
|
+
const dir = localSessionDirectory(options);
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
39
|
+
if (process.platform !== 'win32') {
|
|
40
|
+
try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
|
|
41
|
+
}
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeUsage(value = {}) {
|
|
46
|
+
return {
|
|
47
|
+
input_tokens: Number(value.input_tokens || 0),
|
|
48
|
+
output_tokens: Number(value.output_tokens || 0),
|
|
49
|
+
cached_input_tokens: Number(value.cached_input_tokens || 0),
|
|
50
|
+
cache_write_tokens: Number(value.cache_write_tokens || 0),
|
|
51
|
+
reasoning_tokens: Number(value.reasoning_tokens || 0),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeLocalSession(value = {}) {
|
|
56
|
+
const createdAt = clean(value.created_at) || now();
|
|
57
|
+
return {
|
|
58
|
+
schema_version: LOCAL_SESSION_SCHEMA,
|
|
59
|
+
id: validateSessionId(value.id || createLocalSessionId()),
|
|
60
|
+
status: clean(value.status) || 'active',
|
|
61
|
+
cwd: path.resolve(clean(value.cwd) || process.cwd()),
|
|
62
|
+
title: clean(value.title) || sessionTitleFromInput(value.last_input),
|
|
63
|
+
last_input: clean(value.last_input) || null,
|
|
64
|
+
created_at: createdAt,
|
|
65
|
+
updated_at: clean(value.updated_at) || createdAt,
|
|
66
|
+
model_key: clean(value.model_key) || null,
|
|
67
|
+
provider_model_id: clean(value.provider_model_id) || null,
|
|
68
|
+
reasoning_effort: clean(value.reasoning_effort) || null,
|
|
69
|
+
requested_service_tier: clean(value.requested_service_tier) || null,
|
|
70
|
+
actual_service_tier: clean(value.actual_service_tier) || null,
|
|
71
|
+
provider_state: value.provider_state && typeof value.provider_state === 'object' ? { ...value.provider_state } : {},
|
|
72
|
+
usage_snapshot: value.usage_snapshot && typeof value.usage_snapshot === 'object' ? structuredClone(value.usage_snapshot) : null,
|
|
73
|
+
cumulative_usage: normalizeUsage(value.cumulative_usage || {}),
|
|
74
|
+
total_cost_usd: Number(value.total_cost_usd || 0),
|
|
75
|
+
approved_projected_call_cost_usd: Number(value.approved_projected_call_cost_usd || 0),
|
|
76
|
+
last_error: value.last_error && typeof value.last_error === 'object' ? structuredClone(value.last_error) : null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function saveLocalSession(session, options = {}) {
|
|
81
|
+
const normalized = normalizeLocalSession({ ...session, updated_at: now() });
|
|
82
|
+
ensureDirectory(options);
|
|
83
|
+
const filename = filenameFor(normalized.id, options);
|
|
84
|
+
const temp = `${filename}.${process.pid}.tmp`;
|
|
85
|
+
fs.writeFileSync(temp, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
|
|
86
|
+
if (process.platform !== 'win32') {
|
|
87
|
+
try { fs.chmodSync(temp, 0o600); } catch { /* best effort */ }
|
|
88
|
+
}
|
|
89
|
+
fs.renameSync(temp, filename);
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createLocalSession(value = {}, options = {}) {
|
|
94
|
+
return saveLocalSession(normalizeLocalSession({ ...value, id: value.id || createLocalSessionId(), created_at: now(), updated_at: now() }), options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function loadLocalSession(sessionId, options = {}) {
|
|
98
|
+
const filename = filenameFor(sessionId, options);
|
|
99
|
+
if (!fs.existsSync(filename)) return null;
|
|
100
|
+
const parsed = JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
101
|
+
if (parsed?.schema_version !== LOCAL_SESSION_SCHEMA) throw new Error(`unsupported_local_session_schema:${parsed?.schema_version || 'missing'}`);
|
|
102
|
+
return normalizeLocalSession(parsed);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function updateLocalSession(sessionId, patch = {}, options = {}) {
|
|
106
|
+
const current = loadLocalSession(sessionId, options);
|
|
107
|
+
if (!current) throw new Error(`session_not_found:${sessionId}`);
|
|
108
|
+
return saveLocalSession({ ...current, ...patch, id: current.id, created_at: current.created_at }, options);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function listLocalSessions(options = {}) {
|
|
112
|
+
const dir = localSessionDirectory(options);
|
|
113
|
+
if (!fs.existsSync(dir)) return [];
|
|
114
|
+
const cwd = clean(options.cwd) ? path.resolve(options.cwd) : '';
|
|
115
|
+
const limit = Number.isInteger(options.limit) && options.limit > 0 ? Math.min(options.limit, 100) : 20;
|
|
116
|
+
const rows = [];
|
|
117
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
118
|
+
if (!entry.isFile() || !entry.name.startsWith('asess_') || !entry.name.endsWith('.json')) continue;
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, entry.name), 'utf8'));
|
|
121
|
+
if (parsed?.schema_version !== LOCAL_SESSION_SCHEMA) continue;
|
|
122
|
+
const session = normalizeLocalSession(parsed);
|
|
123
|
+
if (!cwd || session.cwd === cwd) rows.push(session);
|
|
124
|
+
} catch { /* skip corrupt session file */ }
|
|
125
|
+
}
|
|
126
|
+
return rows.sort((a, b) => String(b.updated_at).localeCompare(String(a.updated_at))).slice(0, limit);
|
|
127
|
+
}
|
package/src/lib/local-status.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
package/src/lib/merkle/hash.js
CHANGED
|
@@ -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
|
));
|
package/src/lib/merkle/index.js
CHANGED
|
@@ -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';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const MERKLE_SNAPSHOT_TABLE = 'agentsam_fs_merkle_snapshots';
|
|
2
|
+
export const MERKLE_SNAPSHOT_STORAGE_PREFIX = 'agentsam_fs_merkle_snapshots';
|
|
3
|
+
export const MERKLE_WEBSITE_ASSETS_BINDING = 'WEBSITE_ASSETS';
|
|
4
|
+
|
|
5
|
+
function clean(value) {
|
|
6
|
+
return value == null ? '' : String(value).trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function safeSegment(value, name) {
|
|
10
|
+
const segment = clean(value);
|
|
11
|
+
if (!segment) throw new Error(`${name}_required`);
|
|
12
|
+
if (segment === '.' || segment === '..' || /[\u0000-\u001f\u007f]/.test(segment)) {
|
|
13
|
+
throw new Error(`${name}_invalid`);
|
|
14
|
+
}
|
|
15
|
+
return encodeURIComponent(segment);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeMerkleStoragePrefix(value = MERKLE_SNAPSHOT_STORAGE_PREFIX) {
|
|
19
|
+
const prefix = clean(value || MERKLE_SNAPSHOT_STORAGE_PREFIX).replace(/^\/+|\/+$/g, '');
|
|
20
|
+
if (!prefix || prefix.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
21
|
+
throw new Error('merkle_storage_prefix_invalid');
|
|
22
|
+
}
|
|
23
|
+
return prefix;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Provider-neutral object key. The host chooses which physical bucket is bound
|
|
28
|
+
* to the logical WEBSITE_ASSETS role; the SDK never owns cloud credentials.
|
|
29
|
+
*/
|
|
30
|
+
export function merkleSnapshotStorageKey({ ownerUserId, repoId, snapshotId, prefix = MERKLE_SNAPSHOT_STORAGE_PREFIX } = {}) {
|
|
31
|
+
return `${normalizeMerkleStoragePrefix(prefix)}/${safeSegment(ownerUserId, 'owner_user_id')}/${safeSegment(repoId, 'repo_id')}/${safeSegment(snapshotId, 'snapshot_id')}.json`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Portable D1/SQLite schema for hosts that opt into persisted Merkle snapshots. */
|
|
35
|
+
export const MERKLE_SNAPSHOT_SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS ${MERKLE_SNAPSHOT_TABLE} (
|
|
36
|
+
snapshot_id TEXT PRIMARY KEY NOT NULL,
|
|
37
|
+
owner_user_id TEXT NOT NULL,
|
|
38
|
+
repo_id TEXT NOT NULL,
|
|
39
|
+
repository TEXT,
|
|
40
|
+
source TEXT NOT NULL CHECK (source IN ('github','gitlab','bitbucket','local','upload')),
|
|
41
|
+
manifest_format TEXT NOT NULL DEFAULT 'agentsam-merkle',
|
|
42
|
+
manifest_version INTEGER NOT NULL DEFAULT 1,
|
|
43
|
+
hash_algorithm TEXT NOT NULL DEFAULT 'sha256',
|
|
44
|
+
root_hash TEXT NOT NULL,
|
|
45
|
+
policy_hash TEXT,
|
|
46
|
+
resolved_commit_sha TEXT,
|
|
47
|
+
resolved_tree_sha TEXT,
|
|
48
|
+
git_branch TEXT,
|
|
49
|
+
working_tree_dirty INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
connection_id TEXT,
|
|
51
|
+
runtime_lease_id TEXT,
|
|
52
|
+
storage_backend TEXT NOT NULL DEFAULT 'r2' CHECK (storage_backend IN ('r2','local_fs','inline','none')),
|
|
53
|
+
storage_bucket TEXT,
|
|
54
|
+
storage_key TEXT,
|
|
55
|
+
entry_count INTEGER,
|
|
56
|
+
file_count INTEGER,
|
|
57
|
+
directory_count INTEGER,
|
|
58
|
+
symlink_count INTEGER,
|
|
59
|
+
total_bytes INTEGER,
|
|
60
|
+
capture_kind TEXT NOT NULL DEFAULT 'manual' CHECK (capture_kind IN ('deploy','manual','agent','index')),
|
|
61
|
+
deployment_id TEXT,
|
|
62
|
+
worker_version_id TEXT,
|
|
63
|
+
reference_label TEXT,
|
|
64
|
+
created_at INTEGER NOT NULL,
|
|
65
|
+
persisted_at INTEGER,
|
|
66
|
+
metadata_root TEXT,
|
|
67
|
+
classifier_format TEXT,
|
|
68
|
+
classifier_version INTEGER,
|
|
69
|
+
classifier_source TEXT,
|
|
70
|
+
CHECK (storage_backend IN ('inline','none') OR (storage_bucket IS NOT NULL AND storage_key IS NOT NULL)),
|
|
71
|
+
CHECK (connection_id IS NOT NULL OR runtime_lease_id IS NOT NULL OR capture_kind = 'deploy')
|
|
72
|
+
);`;
|