@inneranimalmedia/agentsam-sdk 2.6.3 → 2.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTSAM.md +6 -0
- package/README.md +16 -1
- package/bin/agentsam +16 -1
- package/docs/BRAND_INTELLIGENCE.md +1 -1
- package/docs/architecture/AGENTSAM_DISTRIBUTION_OWNERSHIP.md +176 -0
- package/docs/architecture/AGENTSAM_GO_RUNTIME.md +282 -0
- package/docs/architecture/CODEBASEINDEX_GUIDED_PATH.md +202 -0
- package/docs/architecture/FS_E2E_CLOSURE_RECEIPT.md +59 -0
- package/docs/architecture/LOCAL_FS_AUTHORITY.md +38 -0
- package/docs/architecture/LOCAL_STUDIO_CLOUDFLARE_OAUTH.md +28 -0
- package/docs/architecture/PLAN_CLI_AND_LOCAL_STUDIO_DESKTOP.md +96 -0
- package/docs/architecture/SAM_ACTIVITY_RECOVERY_RECEIPT.md +39 -0
- package/docs/architecture/SAM_DECISION_WORK_RECEIPT.md +54 -0
- package/docs/architecture/SAM_KERNEL.md +181 -0
- package/docs/architecture/SAM_MACHINE_NORMALIZATION_PRECOMMIT_REPORT.md +208 -0
- package/docs/architecture/SLASH_SKILLS_PORTABLE.md +116 -0
- package/docs/architecture/fs-e2e-receipt.latest.json +42 -0
- package/docs/architecture/previews/codebaseindex-guided-path/CODEBASEINDEX_GUIDED_PATH.md +202 -0
- package/docs/architecture/previews/codebaseindex-guided-path/index.html +321 -0
- package/migrations/d1/0010_portable_tickets_memory.sql +140 -0
- package/migrations/d1/0011_agentsam_skill_v2.sql +185 -0
- package/migrations/d1/0011b_agentsam_skill_v2_cutover.sql +22 -0
- package/migrations/d1/0011c_agentsam_skill_v2_backfill.sql +76 -0
- package/migrations/d1/0011d_agentsam_skill_v2_retrieval_revisions.sql +50 -0
- package/migrations/d1/0012_agentsam_tools_required_seed.sql +67 -0
- package/migrations/d1/0013_identity_oauth_states.sql +15 -0
- package/migrations/d1/0014_auth_event_log.sql +20 -0
- package/migrations/d1/0015_identity_oauth_state_app_id.sql +3 -0
- package/migrations/d1/README_PORTABLE_CONTROL_PLANE.md +16 -0
- package/migrations/sqlite/agentsam_skill_retrieval.portable.sql +42 -0
- package/package.json +19 -4
- package/packages/agentsam-contracts/src/errors.ts +16 -0
- package/packages/agentsam-errors/src/envelope.js +53 -0
- package/packages/agentsam-errors/src/index.js +1 -0
- package/packages/agentsam-errors/src/recovery.js +301 -0
- package/packages/agentsam-knowledge/src/providers/index.js +18 -6
- package/packages/connectors/cloudflare/src/routes.js +9 -0
- package/packages/connectors/cloudflare/tests/connector.test.mjs +26 -1
- package/packages/identity/.agentsam/features/oauth-login-portal/agentsam.feature.json +1 -1
- package/packages/identity/.agentsam/features/oauth-login-portal/routes.json +11 -2
- package/packages/identity/docs/PORTABLE_IDENTITY_ARCHITECTURE.md +50 -0
- package/packages/identity/migrations/D1_SCHEMA_MAPPING.md +31 -0
- package/packages/identity/migrations/sqlite/001_identity_core.sql +99 -0
- package/packages/identity/migrations/sqlite/002_identity_oauth_client.sql +38 -0
- package/packages/identity/migrations/sqlite/003_identity_oauth_server.sql +64 -0
- package/packages/identity/package.json +2 -2
- package/packages/identity/src/adapters/cloudflare-d1/index.js +122 -22
- package/packages/identity/src/adapters/sqlite/index.js +319 -0
- package/packages/identity/src/app/verify-app.js +95 -0
- package/packages/identity/src/contracts/identity-store.js +115 -0
- package/packages/identity/src/contracts/route-ids.js +30 -0
- package/packages/identity/src/contracts/route-projection.js +218 -0
- package/packages/identity/src/contracts/routes.js +11 -0
- package/packages/identity/src/core/browser-paths.js +4 -5
- package/packages/identity/src/core/constants.js +13 -8
- package/packages/identity/src/core/session-policy.js +32 -0
- package/packages/identity/src/frontend/auth-portal/pages/login.html +10 -10
- package/packages/identity/src/frontend/auth-portal/pages/reset.html +3 -3
- package/packages/identity/src/frontend/auth-portal/pages/signup.html +3 -3
- package/packages/identity/src/frontend/auth-portal/preview/dashboard-stub.html +1 -1
- package/packages/identity/src/index.js +25 -0
- package/packages/identity/src/oauth/README.md +10 -4
- package/packages/identity/src/oauth/credentials.js +20 -13
- package/packages/identity/src/oauth/finalize-inbound.js +1 -1
- package/packages/identity/src/oauth/iam-platform.js +8 -7
- package/packages/identity/src/oauth/redirect-paths.js +27 -36
- package/packages/identity/src/server/identity-service.js +30 -13
- package/packages/identity/src/server/mount-policy.js +30 -0
- package/packages/identity/src/server/post-auth.js +79 -0
- package/packages/identity/src/server/worker-router.js +104 -72
- package/packages/identity/tests/finalize-inbound-oauth.test.mjs +6 -6
- package/packages/identity/tests/iam-provider.test.mjs +1 -1
- package/packages/identity/tests/identity-service.test.mjs +36 -5
- package/packages/identity/tests/oauth-credentials.test.mjs +3 -1
- package/packages/identity/tests/portable-identity-architecture.test.mjs +157 -0
- package/packages/identity/tests/session-routes-policy.test.mjs +21 -0
- package/packages/theme-church-site/package.json +2 -1
- package/packages/theme-church-site/src/index.js +1 -0
- package/packages/theme-companions-site/package.json +2 -1
- package/packages/theme-companions-site/src/index.js +1 -0
- package/packages/theme-floors-site/package.json +2 -1
- package/packages/theme-floors-site/src/index.js +1 -0
- package/packages/theme-fuelnfree-site/package.json +2 -1
- package/packages/theme-fuelnfree-site/src/index.js +1 -0
- package/packages/theme-handyman-site/package.json +2 -1
- package/packages/theme-handyman-site/src/index.js +1 -0
- package/packages/theme-insurance-site/package.json +2 -1
- package/packages/theme-insurance-site/src/index.js +1 -0
- package/packages/theme-shinshu-site/package.json +2 -1
- package/packages/theme-shinshu-site/src/index.js +1 -0
- package/protocol/apps/agentsam.app.v1.schema.json +51 -0
- package/protocol/brand/brandpack.v1.schema.json +43 -0
- package/protocol/credentials/issue.v1.schema.json +38 -0
- package/protocol/database/connection.v1.schema.json +41 -0
- package/protocol/embeddings/embedding-profile.v1.schema.json +20 -0
- package/protocol/errors/error-envelope.schema.json +135 -1
- package/protocol/errors/recovery.v1.schema.json +50 -0
- package/protocol/runtime/workspace-fs.v1.schema.json +71 -0
- package/protocol/sam/activity.v1.schema.json +48 -0
- package/protocol/sam/answer.v1.schema.json +35 -0
- package/protocol/sam/calibration.v1.schema.json +21 -0
- package/protocol/sam/decision-receipt.v1.schema.json +29 -0
- package/protocol/sam/evaluation.v1.schema.json +19 -0
- package/protocol/sam/operation.schema.json +66 -0
- package/protocol/sam/outcome.v1.schema.json +36 -0
- package/protocol/sam/question.v1.schema.json +26 -0
- package/protocol/sam/registry.seed.json +153 -0
- package/protocol/sam/result.schema.json +53 -0
- package/protocol/sam/state.v1.schema.json +23 -0
- package/protocol/skills/agentsam.interaction.v1.schema.json +50 -0
- package/protocol/skills/agentsam.skill.v1.schema.json +57 -0
- package/protocol/ui/icon-registry.mjs +226 -0
- package/protocol/ui/icon.v1.schema.json +51 -0
- package/skills/README.md +22 -9
- package/skills/agentsam-codebaseindex/SKILL.md +225 -0
- package/skills/catalog.json +14 -0
- package/src/cli/command-catalog.js +130 -0
- package/src/cli/dispatch.js +48 -0
- package/src/cli.js +43 -1
- package/src/commands/api-key.js +244 -0
- package/src/commands/app.js +60 -28
- package/src/commands/brand.js +17 -19
- package/src/commands/codebaseindex.js +688 -0
- package/src/commands/env.js +152 -25
- package/src/commands/go.js +366 -53
- package/src/commands/interaction-clack.js +115 -0
- package/src/commands/models.js +1 -1
- package/src/commands/providers.js +62 -14
- package/src/commands/shell.js +43 -2
- package/src/commands/skill.js +248 -0
- package/src/commands/skills.js +1 -1
- package/src/commands/start-local.js +4 -0
- package/src/commands/whoami.js +90 -18
- package/src/go/build.js +229 -39
- package/src/go/cloudflare.js +506 -105
- package/src/go/container.js +120 -0
- package/src/go/contract.js +9 -4
- package/src/go/discover.js +149 -33
- package/src/go/index.js +15 -3
- package/src/go/native-probe-runner.mjs +119 -0
- package/src/go/{registry.js → official-registry.js} +64 -7
- package/src/go/official-release.js +10 -0
- package/src/go/receipts.js +77 -10
- package/src/go/verify.js +9 -2
- package/src/index.js +27 -0
- package/src/indexing/ingest/discover-models.js +298 -0
- package/src/indexing/ingest/inventory.js +243 -0
- package/src/indexing/ingest/job-graph.js +181 -0
- package/src/indexing/ingest/materials.js +210 -0
- package/src/lib/provider-credentials.js +63 -21
- package/src/lib/slash-commands.js +1 -0
- package/src/local-fs/capability.js +121 -0
- package/src/local-fs/freshness.js +75 -0
- package/src/local-fs/index.js +385 -0
- package/src/local-fs/paths.js +100 -0
- package/src/local-pty/server.js +295 -19
- package/src/mcp/client.js +2 -2
- package/src/models/ai-access-onboarding.js +112 -0
- package/src/models/discovery.js +10 -2
- package/src/models/inventory-core.js +9 -1
- package/src/sam/activity/index.js +183 -0
- package/src/sam/client.js +252 -0
- package/src/sam/decision/calibration.js +109 -0
- package/src/sam/decision/confidence.js +126 -0
- package/src/sam/decision/evaluate.js +157 -0
- package/src/sam/decision/evaluators/deterministic.js +341 -0
- package/src/sam/decision/evaluators/heuristic.js +61 -0
- package/src/sam/decision/evaluators/select.js +50 -0
- package/src/sam/decision/evaluators/semantic.js +149 -0
- package/src/sam/decision/hierarchical.js +61 -0
- package/src/sam/decision/index.js +53 -0
- package/src/sam/decision/policy.js +86 -0
- package/src/sam/decision/questions.js +120 -0
- package/src/sam/decision/receipt.js +148 -0
- package/src/sam/decision/state.js +117 -0
- package/src/sam/decision/types.js +22 -0
- package/src/sam/decision/validate.js +200 -0
- package/src/sam/define.js +51 -0
- package/src/sam/index.js +65 -0
- package/src/sam/operations/brand-scan.js +62 -0
- package/src/sam/operations/cad-blender-inspect.js +36 -0
- package/src/sam/operations/codebaseindex-ingest.js +49 -0
- package/src/sam/operations/decision-evaluate.js +59 -0
- package/src/sam/operations/planning-astar.js +77 -0
- package/src/sam/operations/planning-goap.js +60 -0
- package/src/sam/operations/repository-inspect.js +72 -0
- package/src/sam/operations/security-scan.js +33 -0
- package/src/sam/operations/terminal-exec.js +29 -0
- package/src/sam/planning/astar.js +311 -0
- package/src/sam/planning/goap.js +177 -0
- package/src/sam/planning/index.js +21 -0
- package/src/sam/planning/state.js +84 -0
- package/src/sam/registry.js +48 -0
- package/src/sam/result.js +77 -0
- package/src/sam/seed.js +44 -0
- package/src/sam/types.js +91 -0
- package/src/skills/catalog.js +64 -0
- package/src/skills/content-resolver.js +124 -0
- package/src/skills/hosted-store.js +37 -0
- package/src/skills/index.js +29 -64
- package/src/skills/interaction.js +102 -0
- package/src/skills/local-store.js +228 -0
- package/src/skills/manifest.js +104 -0
- package/src/skills/metrics.js +31 -0
- package/src/skills/registry.js +184 -0
- package/src/skills/runtime.js +287 -0
- package/src/skills/slash.js +44 -0
- package/src/ui/cli/help.js +94 -101
- package/test/cli/api-key-env-whoami.test.mjs +129 -0
- package/test/cli/codebaseindex-plan-ux.test.mjs +37 -0
- package/test/cli/go.test.mjs +62 -5
- package/test/cli/skill-npm-and-env.test.mjs +52 -0
- package/test/cli/wireframes-go-registry.test.mjs +87 -2
- package/test/go/build-source-identity.test.mjs +31 -0
- package/test/go/cloudflare-probe.test.mjs +274 -10
- package/test/go/distribution.test.mjs +28 -0
- package/test/integration/ai-access-onboarding.test.mjs +47 -0
- package/test/integration/cli-help.test.mjs +1 -1
- package/test/integration/cms-site-tenancy-contract.test.mjs +6 -6
- package/test/integration/codebaseindex-ingest.test.mjs +166 -0
- package/test/integration/icon-registry.test.mjs +67 -0
- package/test/integration/ingest-discover-models.test.mjs +30 -0
- package/test/integration/install-script.test.mjs +12 -9
- package/test/integration/local-fs.test.mjs +113 -0
- package/test/integration/provider-env-cli.test.mjs +2 -1
- package/test/integration/sam-activity-recovery.test.mjs +147 -0
- package/test/integration/sam-decision.test.mjs +584 -0
- package/test/integration/sam-kernel.test.mjs +99 -0
- package/test/integration/sam-planning-astar.test.mjs +279 -0
- package/test/integration/skill-runtime.test.mjs +240 -0
- package/test/integration/studio-fs-pty-e2e.test.mjs +294 -0
- package/test/models.test.mjs +14 -7
- package/test/shell.test.mjs +4 -4
|
@@ -129,28 +129,17 @@ export function agentEnvLoaderPath(options = {}) {
|
|
|
129
129
|
|
|
130
130
|
export function ensureAgentEnvLoader(options = {}) {
|
|
131
131
|
const filename = agentEnvLoaderPath(options);
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
const source = `# AgentSam environment loader. Source this file; do not execute it.
|
|
133
|
+
# Credentials resolve from the OS keychain / local vault via \`agentsam env shell\`.
|
|
134
|
+
# Do not put plaintext secrets in this file.
|
|
135
|
+
if command -v agentsam >/dev/null 2>&1; then
|
|
136
|
+
eval "$(agentsam env shell --profile default)"
|
|
137
|
+
elif command -v agentsam-env >/dev/null 2>&1; then
|
|
138
|
+
eval "$(agentsam-env shell --profile default)"
|
|
139
|
+
else
|
|
140
|
+
echo "agentsam CLI not found on PATH; cannot load AgentSam credentials" >&2
|
|
141
|
+
return 1 2>/dev/null || exit 1
|
|
137
142
|
fi
|
|
138
|
-
for _agentsam_profile in "$@"; do
|
|
139
|
-
case "$_agentsam_profile" in
|
|
140
|
-
grok) _agentsam_profile="xai" ;;
|
|
141
|
-
${supported}) ;;
|
|
142
|
-
*) echo "unknown AgentSam provider profile: $_agentsam_profile" >&2; return 2 2>/dev/null || exit 2 ;;
|
|
143
|
-
esac
|
|
144
|
-
_agentsam_file="\${HOME}/.agentsam/env.d/\${_agentsam_profile}.env"
|
|
145
|
-
if [ ! -f "$_agentsam_file" ]; then
|
|
146
|
-
echo "AgentSam provider profile not found: $_agentsam_file" >&2
|
|
147
|
-
return 1 2>/dev/null || exit 1
|
|
148
|
-
fi
|
|
149
|
-
set -a
|
|
150
|
-
. "$_agentsam_file"
|
|
151
|
-
set +a
|
|
152
|
-
done
|
|
153
|
-
unset _agentsam_file _agentsam_profile
|
|
154
143
|
`;
|
|
155
144
|
atomicWrite(filename, source, 0o700);
|
|
156
145
|
return filename;
|
|
@@ -359,3 +348,56 @@ export function describeProviderCredential(provider, options = {}) {
|
|
|
359
348
|
export function listProviderCredentialStatus(options = {}) {
|
|
360
349
|
return Object.freeze(Object.keys(PROVIDER_CREDENTIALS).map((provider) => describeProviderCredential(provider, options)));
|
|
361
350
|
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Emit shell `export` lines for configured credentials from vault/keychain/env.
|
|
354
|
+
* Bootstrap-safe: does not require AGENTSAM_API_KEY already present to resolve it.
|
|
355
|
+
* Prefer vault/OS store over ambient env when `preferVault` is true (default for shell).
|
|
356
|
+
*/
|
|
357
|
+
export function renderEnvShellExports(options = {}) {
|
|
358
|
+
const profile = clean(options.profile || 'default').toLowerCase();
|
|
359
|
+
const wanted = Array.isArray(options.providers) && options.providers.length
|
|
360
|
+
? options.providers.map(normalizeProviderId).filter(Boolean)
|
|
361
|
+
: profile === 'default'
|
|
362
|
+
? Object.keys(PROVIDER_CREDENTIALS)
|
|
363
|
+
: profile.split(/[,\s]+/).map(normalizeProviderId).filter(Boolean);
|
|
364
|
+
|
|
365
|
+
const lines = ['# AgentSam env shell — evaluated by: eval "$(agentsam env shell)"'];
|
|
366
|
+
const emitted = [];
|
|
367
|
+
|
|
368
|
+
for (const provider of wanted) {
|
|
369
|
+
const spec = providerCredentialSpec(provider);
|
|
370
|
+
if (!spec) continue;
|
|
371
|
+
|
|
372
|
+
let resolved;
|
|
373
|
+
if (options.preferVault !== false) {
|
|
374
|
+
// Skip ambient process.env so a stale empty shell does not block keychain.
|
|
375
|
+
const vaultOnly = { ...options, env: { ...(options.env || {}), [spec.env]: undefined } };
|
|
376
|
+
if (spec.accountEnv) {
|
|
377
|
+
for (const name of spec.accountEnv) vaultOnly.env[name] = undefined;
|
|
378
|
+
}
|
|
379
|
+
resolved = resolveProviderCredential(spec.provider, vaultOnly);
|
|
380
|
+
if (!resolved.configured) {
|
|
381
|
+
resolved = resolveProviderCredential(spec.provider, options);
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
resolved = resolveProviderCredential(spec.provider, options);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!resolved.configured || !resolved.value) continue;
|
|
388
|
+
lines.push(`export ${spec.env}=${envLiteral(resolved.value)}`);
|
|
389
|
+
if (spec.provider === 'cloudflare' && resolved.account_id) {
|
|
390
|
+
lines.push(`export CLOUDFLARE_ACCOUNT_ID=${envLiteral(resolved.account_id)}`);
|
|
391
|
+
}
|
|
392
|
+
emitted.push(spec.provider);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!emitted.length) {
|
|
396
|
+
lines.push('# No AgentSam credentials configured. Run: agentsam api-key create --store keychain --activate');
|
|
397
|
+
}
|
|
398
|
+
return Object.freeze({
|
|
399
|
+
profile,
|
|
400
|
+
providers: emitted,
|
|
401
|
+
script: `${lines.join('\n')}\n`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
@@ -35,6 +35,7 @@ export const SLASH_COMMANDS = [
|
|
|
35
35
|
{ cmd: '/logs', description: 'Show local Agent Sam execution events', lane: 'observability' },
|
|
36
36
|
{ cmd: '/deploy', description: 'Add a cloud adapter and deploy intentionally', lane: 'deploy' },
|
|
37
37
|
{ cmd: '/clear', description: 'Clear the terminal display', lane: 'terminal' },
|
|
38
|
+
{ cmd: '/skills', description: 'List resolvable slash skill vocabulary', lane: 'agent' },
|
|
38
39
|
{ cmd: '/help', description: 'Show the factual implemented command catalog' },
|
|
39
40
|
{ cmd: '/exit', description: 'Exit Agent Sam shell and return to the host terminal' },
|
|
40
41
|
];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local runtime workspace capability — short-lived FS/PTY authority for one root.
|
|
3
|
+
*
|
|
4
|
+
* Compatible with later:
|
|
5
|
+
* account → terminal connection → enrolled instance → approved workspace root
|
|
6
|
+
*
|
|
7
|
+
* Local today:
|
|
8
|
+
* start-local binds one root + mints one capability.
|
|
9
|
+
* Browser cannot invent an absolute root to gain authority.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import crypto from 'node:crypto';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
export const WORKSPACE_CAPABILITY_HEADER = 'x-agentsam-workspace-capability';
|
|
17
|
+
export const LOCAL_RUNTIME_FILE = 'local-runtime.json';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{ root: string, port: number, host?: string }} opts
|
|
21
|
+
*/
|
|
22
|
+
export function mintWorkspaceCapability(opts) {
|
|
23
|
+
const root = path.resolve(opts.root);
|
|
24
|
+
const workspace_id = `ws_${crypto.randomBytes(10).toString('hex')}`;
|
|
25
|
+
const capability = `awc_${crypto.randomBytes(24).toString('hex')}`;
|
|
26
|
+
const expires_at = new Date(Date.now() + 12 * 60 * 60 * 1000).toISOString();
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
schema: 'agentsam.local-workspace-capability.v1',
|
|
29
|
+
workspace_id,
|
|
30
|
+
capability,
|
|
31
|
+
root,
|
|
32
|
+
host: opts.host || '127.0.0.1',
|
|
33
|
+
port: opts.port,
|
|
34
|
+
runtimeBaseUrl: `http://${opts.host || '127.0.0.1'}:${opts.port}`,
|
|
35
|
+
fsBaseUrl: `http://${opts.host || '127.0.0.1'}:${opts.port}/v1/fs`,
|
|
36
|
+
expires_at,
|
|
37
|
+
purpose: 'local.filesystem+pty',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Persist capability beside the authorized root (machine-local; not file content authority).
|
|
43
|
+
* @param {object} record
|
|
44
|
+
*/
|
|
45
|
+
export function writeLocalRuntimeRecord(record) {
|
|
46
|
+
const dir = path.join(record.root, '.agentsam');
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
const file = path.join(dir, LOCAL_RUNTIME_FILE);
|
|
49
|
+
fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
50
|
+
return file;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} root
|
|
55
|
+
*/
|
|
56
|
+
export function readLocalRuntimeRecord(root) {
|
|
57
|
+
const file = path.join(path.resolve(root), '.agentsam', LOCAL_RUNTIME_FILE);
|
|
58
|
+
if (!fs.existsSync(file)) return null;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {import('http').IncomingMessage} req
|
|
68
|
+
* @param {string} expected
|
|
69
|
+
*/
|
|
70
|
+
export function extractCapability(req, expected) {
|
|
71
|
+
const header = req.headers[WORKSPACE_CAPABILITY_HEADER]
|
|
72
|
+
|| req.headers.authorization
|
|
73
|
+
|| '';
|
|
74
|
+
let token = '';
|
|
75
|
+
if (typeof header === 'string') {
|
|
76
|
+
if (header.toLowerCase().startsWith('bearer ')) token = header.slice(7).trim();
|
|
77
|
+
else token = header.trim();
|
|
78
|
+
}
|
|
79
|
+
if (!token && req.url) {
|
|
80
|
+
try {
|
|
81
|
+
const u = new URL(req.url, 'http://127.0.0.1');
|
|
82
|
+
token = u.searchParams.get('capability') || '';
|
|
83
|
+
} catch {
|
|
84
|
+
/* ignore */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
ok: Boolean(expected && token && token === expected),
|
|
89
|
+
token,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Loopback + Studio origin allowlist. Never `*`.
|
|
95
|
+
* @param {string|undefined} origin
|
|
96
|
+
* @param {string[]} [extra]
|
|
97
|
+
*/
|
|
98
|
+
export function isAllowedStudioOrigin(origin, extra = []) {
|
|
99
|
+
if (!origin || origin === 'null') return true; // non-browser / same-origin tooling
|
|
100
|
+
const allow = [
|
|
101
|
+
'http://127.0.0.1:8080',
|
|
102
|
+
'http://localhost:8080',
|
|
103
|
+
'http://127.0.0.1:5173',
|
|
104
|
+
'http://localhost:5173',
|
|
105
|
+
'http://127.0.0.1:3000',
|
|
106
|
+
'http://localhost:3000',
|
|
107
|
+
...extra,
|
|
108
|
+
];
|
|
109
|
+
return allow.includes(origin);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {import('http').IncomingMessage} req
|
|
114
|
+
*/
|
|
115
|
+
export function isLoopbackRemote(req) {
|
|
116
|
+
const raw = req.socket?.remoteAddress || '';
|
|
117
|
+
return raw === '127.0.0.1'
|
|
118
|
+
|| raw === '::1'
|
|
119
|
+
|| raw === '::ffff:127.0.0.1'
|
|
120
|
+
|| raw.endsWith('127.0.0.1');
|
|
121
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal repository/index freshness bridge.
|
|
3
|
+
* Filesystem writes mark generation stale — do not re-index on every keystroke.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {Map<string, { generation: number, stale_paths: Set<string>, updated_at: string }>} */
|
|
7
|
+
const roots = new Map();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} root
|
|
11
|
+
* @param {{ paths?: string[], reason?: string }} [opts]
|
|
12
|
+
*/
|
|
13
|
+
export function markRepositoryStale(root, opts = {}) {
|
|
14
|
+
const key = String(root);
|
|
15
|
+
const prev = roots.get(key) || {
|
|
16
|
+
generation: 0,
|
|
17
|
+
stale_paths: new Set(),
|
|
18
|
+
updated_at: null,
|
|
19
|
+
};
|
|
20
|
+
const nextGen = prev.generation + 1;
|
|
21
|
+
const stale_paths = new Set(prev.stale_paths);
|
|
22
|
+
for (const p of opts.paths || []) stale_paths.add(String(p));
|
|
23
|
+
const record = {
|
|
24
|
+
generation: nextGen,
|
|
25
|
+
stale_paths,
|
|
26
|
+
updated_at: new Date().toISOString(),
|
|
27
|
+
reason: opts.reason || 'filesystem_write',
|
|
28
|
+
};
|
|
29
|
+
roots.set(key, record);
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
schema: 'agentsam.repository-freshness.v1',
|
|
33
|
+
root: key,
|
|
34
|
+
generation: record.generation,
|
|
35
|
+
stale_paths: [...stale_paths].slice(0, 200),
|
|
36
|
+
updated_at: record.updated_at,
|
|
37
|
+
reason: record.reason,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} root
|
|
43
|
+
*/
|
|
44
|
+
export function getRepositoryFreshness(root) {
|
|
45
|
+
const key = String(root);
|
|
46
|
+
const record = roots.get(key);
|
|
47
|
+
if (!record) {
|
|
48
|
+
return {
|
|
49
|
+
ok: true,
|
|
50
|
+
schema: 'agentsam.repository-freshness.v1',
|
|
51
|
+
root: key,
|
|
52
|
+
generation: 0,
|
|
53
|
+
stale: false,
|
|
54
|
+
stale_paths: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
schema: 'agentsam.repository-freshness.v1',
|
|
60
|
+
root: key,
|
|
61
|
+
generation: record.generation,
|
|
62
|
+
stale: record.stale_paths.size > 0 || record.generation > 0,
|
|
63
|
+
stale_paths: [...record.stale_paths].slice(0, 200),
|
|
64
|
+
updated_at: record.updated_at,
|
|
65
|
+
reason: record.reason,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} root
|
|
71
|
+
*/
|
|
72
|
+
export function clearRepositoryFreshness(root) {
|
|
73
|
+
roots.delete(String(root));
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local machine filesystem operations under an authorized workspace root.
|
|
3
|
+
* Version = sha256 of file bytes (content-addressed optimistic concurrency).
|
|
4
|
+
*/
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { resolveContainedPath } from './paths.js';
|
|
10
|
+
import { markRepositoryStale } from './freshness.js';
|
|
11
|
+
|
|
12
|
+
export const LOCAL_FS_ENGINE = 'agentsam-local-fs-v1';
|
|
13
|
+
export const MAX_TEXT_BYTES = 2 * 1024 * 1024; // 2 MiB text default
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} root
|
|
17
|
+
*/
|
|
18
|
+
export function createLocalFilesystem(root) {
|
|
19
|
+
const resolvedRoot = path.resolve(root);
|
|
20
|
+
|
|
21
|
+
function contain(rel) {
|
|
22
|
+
return resolveContainedPath(resolvedRoot, rel);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hashBytes(buf) {
|
|
26
|
+
return `sha256:${crypto.createHash('sha256').update(buf).digest('hex')}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} relPath
|
|
31
|
+
* @param {{ recursive?: boolean, maxDepth?: number }} [opts]
|
|
32
|
+
*/
|
|
33
|
+
function list(relPath = '.', opts = {}) {
|
|
34
|
+
const gate = contain(relPath);
|
|
35
|
+
if (!gate.ok) return gate;
|
|
36
|
+
const recursive = Boolean(opts.recursive);
|
|
37
|
+
const maxDepth = opts.maxDepth ?? 6;
|
|
38
|
+
/** @type {Array<{ path: string, kind: string, size: number, mtime: number }>} */
|
|
39
|
+
const items = [];
|
|
40
|
+
const skip = new Set(['.git', 'node_modules', '.output', 'dist', '.DS_Store']);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} abs
|
|
44
|
+
* @param {string} rel
|
|
45
|
+
* @param {number} depth
|
|
46
|
+
*/
|
|
47
|
+
function walk(abs, rel, depth) {
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
error: err instanceof Error ? err.message : String(err),
|
|
55
|
+
code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'list_failed',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
for (const e of entries) {
|
|
59
|
+
if (skip.has(e.name) || e.name === '.DS_Store') continue;
|
|
60
|
+
const childRel = rel === '.' ? e.name : `${rel}/${e.name}`;
|
|
61
|
+
const childAbs = path.join(abs, e.name);
|
|
62
|
+
let size = 0;
|
|
63
|
+
let mtime = 0;
|
|
64
|
+
try {
|
|
65
|
+
const st = fs.lstatSync(childAbs);
|
|
66
|
+
size = st.size;
|
|
67
|
+
mtime = Math.floor(st.mtimeMs);
|
|
68
|
+
} catch {
|
|
69
|
+
/* ignore */
|
|
70
|
+
}
|
|
71
|
+
const kind = e.isDirectory() ? 'directory' : e.isSymbolicLink() ? 'symlink' : 'file';
|
|
72
|
+
items.push({ path: childRel, kind, size, mtime });
|
|
73
|
+
if (recursive && kind === 'directory' && depth < maxDepth) {
|
|
74
|
+
const nested = walk(childAbs, childRel, depth + 1);
|
|
75
|
+
if (nested && nested.ok === false) return nested;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const walked = walk(gate.abs, gate.rel, 0);
|
|
82
|
+
if (walked && walked.ok === false) return walked;
|
|
83
|
+
|
|
84
|
+
items.sort((a, b) => {
|
|
85
|
+
if (a.kind !== b.kind) return a.kind === 'directory' ? -1 : 1;
|
|
86
|
+
return a.path.localeCompare(b.path);
|
|
87
|
+
});
|
|
88
|
+
return { ok: true, root: resolvedRoot, path: gate.rel, entries: items, engine: LOCAL_FS_ENGINE };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} relPath
|
|
93
|
+
*/
|
|
94
|
+
function stat(relPath) {
|
|
95
|
+
const gate = contain(relPath);
|
|
96
|
+
if (!gate.ok) return gate;
|
|
97
|
+
try {
|
|
98
|
+
const st = fs.lstatSync(gate.abs);
|
|
99
|
+
const kind = st.isDirectory() ? 'directory' : st.isSymbolicLink() ? 'symlink' : 'file';
|
|
100
|
+
let content_hash = null;
|
|
101
|
+
if (kind === 'file' && st.size <= MAX_TEXT_BYTES) {
|
|
102
|
+
content_hash = hashBytes(fs.readFileSync(gate.abs));
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
path: gate.rel,
|
|
107
|
+
kind,
|
|
108
|
+
size: st.size,
|
|
109
|
+
mtime: Math.floor(st.mtimeMs),
|
|
110
|
+
content_hash,
|
|
111
|
+
binary: kind === 'file' && st.size > 0 ? null : false,
|
|
112
|
+
engine: LOCAL_FS_ENGINE,
|
|
113
|
+
};
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: err instanceof Error ? err.message : String(err),
|
|
118
|
+
code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'stat_failed',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} relPath
|
|
125
|
+
* @param {{ maxBytes?: number }} [opts]
|
|
126
|
+
*/
|
|
127
|
+
function read(relPath, opts = {}) {
|
|
128
|
+
const gate = contain(relPath);
|
|
129
|
+
if (!gate.ok) return gate;
|
|
130
|
+
const maxBytes = opts.maxBytes ?? MAX_TEXT_BYTES;
|
|
131
|
+
try {
|
|
132
|
+
const st = fs.statSync(gate.abs);
|
|
133
|
+
if (st.isDirectory()) {
|
|
134
|
+
return { ok: false, error: 'is_directory', code: 'is_directory' };
|
|
135
|
+
}
|
|
136
|
+
if (st.size > maxBytes) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: `file_too_large:${st.size}>${maxBytes}`,
|
|
140
|
+
code: 'file_too_large',
|
|
141
|
+
size: st.size,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const buf = fs.readFileSync(gate.abs);
|
|
145
|
+
// Detect obvious binary
|
|
146
|
+
const sample = buf.subarray(0, Math.min(buf.length, 8000));
|
|
147
|
+
if (sample.includes(0)) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: 'binary_file',
|
|
151
|
+
code: 'binary_file',
|
|
152
|
+
size: st.size,
|
|
153
|
+
content_hash: hashBytes(buf),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const content = buf.toString('utf8');
|
|
157
|
+
const version = hashBytes(buf);
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
path: gate.rel,
|
|
161
|
+
content,
|
|
162
|
+
version,
|
|
163
|
+
content_hash: version,
|
|
164
|
+
mtime: Math.floor(st.mtimeMs),
|
|
165
|
+
size: st.size,
|
|
166
|
+
engine: LOCAL_FS_ENGINE,
|
|
167
|
+
};
|
|
168
|
+
} catch (err) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: err instanceof Error ? err.message : String(err),
|
|
172
|
+
code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'read_failed',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Atomic write with optional optimistic concurrency (expectedVersion).
|
|
179
|
+
* @param {string} relPath
|
|
180
|
+
* @param {string} content
|
|
181
|
+
* @param {{ expectedVersion?: string|null, overwrite?: boolean }} [opts]
|
|
182
|
+
*/
|
|
183
|
+
function write(relPath, content, opts = {}) {
|
|
184
|
+
const gate = contain(relPath);
|
|
185
|
+
if (!gate.ok) return gate;
|
|
186
|
+
if (typeof content !== 'string') {
|
|
187
|
+
return { ok: false, error: 'content_must_be_string', code: 'invalid_input' };
|
|
188
|
+
}
|
|
189
|
+
const buf = Buffer.from(content, 'utf8');
|
|
190
|
+
if (buf.length > MAX_TEXT_BYTES) {
|
|
191
|
+
return { ok: false, error: 'content_too_large', code: 'file_too_large' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const exists = fs.existsSync(gate.abs);
|
|
195
|
+
if (exists) {
|
|
196
|
+
const current = fs.readFileSync(gate.abs);
|
|
197
|
+
const currentVersion = hashBytes(current);
|
|
198
|
+
if (opts.expectedVersion != null && opts.expectedVersion !== '' && opts.expectedVersion !== currentVersion) {
|
|
199
|
+
if (!opts.overwrite) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
error: 'version_conflict',
|
|
203
|
+
code: 'version_conflict',
|
|
204
|
+
expected: opts.expectedVersion,
|
|
205
|
+
actual: currentVersion,
|
|
206
|
+
path: gate.rel,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} else if (opts.expectedVersion) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
error: 'version_conflict',
|
|
214
|
+
code: 'version_conflict',
|
|
215
|
+
expected: opts.expectedVersion,
|
|
216
|
+
actual: null,
|
|
217
|
+
path: gate.rel,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
fs.mkdirSync(path.dirname(gate.abs), { recursive: true });
|
|
223
|
+
const tmp = path.join(
|
|
224
|
+
path.dirname(gate.abs),
|
|
225
|
+
`.agentsam-write-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`,
|
|
226
|
+
);
|
|
227
|
+
fs.writeFileSync(tmp, buf);
|
|
228
|
+
fs.renameSync(tmp, gate.abs);
|
|
229
|
+
const version = hashBytes(buf);
|
|
230
|
+
const st = fs.statSync(gate.abs);
|
|
231
|
+
markRepositoryStale(resolvedRoot, { paths: [gate.rel], reason: 'write' });
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
path: gate.rel,
|
|
235
|
+
version,
|
|
236
|
+
content_hash: version,
|
|
237
|
+
mtime: Math.floor(st.mtimeMs),
|
|
238
|
+
size: st.size,
|
|
239
|
+
engine: LOCAL_FS_ENGINE,
|
|
240
|
+
};
|
|
241
|
+
} catch (err) {
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
error: err instanceof Error ? err.message : String(err),
|
|
245
|
+
code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'write_failed',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {string} relPath
|
|
252
|
+
* @param {string} [content]
|
|
253
|
+
*/
|
|
254
|
+
function create(relPath, content = '') {
|
|
255
|
+
const gate = contain(relPath);
|
|
256
|
+
if (!gate.ok) return gate;
|
|
257
|
+
if (fs.existsSync(gate.abs)) {
|
|
258
|
+
return { ok: false, error: 'already_exists', code: 'already_exists', path: gate.rel };
|
|
259
|
+
}
|
|
260
|
+
return write(relPath, content, {});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @param {string} fromRel
|
|
265
|
+
* @param {string} toRel
|
|
266
|
+
* @param {{ expectedVersion?: string|null }} [opts]
|
|
267
|
+
*/
|
|
268
|
+
function rename(fromRel, toRel, opts = {}) {
|
|
269
|
+
const from = contain(fromRel);
|
|
270
|
+
if (!from.ok) return from;
|
|
271
|
+
const to = contain(toRel);
|
|
272
|
+
if (!to.ok) return to;
|
|
273
|
+
if (!fs.existsSync(from.abs)) {
|
|
274
|
+
return { ok: false, error: 'not_found', code: 'ENOENT', path: from.rel };
|
|
275
|
+
}
|
|
276
|
+
if (opts.expectedVersion) {
|
|
277
|
+
const st = fs.statSync(from.abs);
|
|
278
|
+
if (st.isFile()) {
|
|
279
|
+
const current = hashBytes(fs.readFileSync(from.abs));
|
|
280
|
+
if (current !== opts.expectedVersion) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
error: 'version_conflict',
|
|
284
|
+
code: 'version_conflict',
|
|
285
|
+
expected: opts.expectedVersion,
|
|
286
|
+
actual: current,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (fs.existsSync(to.abs)) {
|
|
292
|
+
return { ok: false, error: 'target_exists', code: 'already_exists', path: to.rel };
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
fs.mkdirSync(path.dirname(to.abs), { recursive: true });
|
|
296
|
+
fs.renameSync(from.abs, to.abs);
|
|
297
|
+
markRepositoryStale(resolvedRoot, { paths: [from.rel, to.rel], reason: 'rename' });
|
|
298
|
+
return { ok: true, from: from.rel, to: to.rel, engine: LOCAL_FS_ENGINE };
|
|
299
|
+
} catch (err) {
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
error: err instanceof Error ? err.message : String(err),
|
|
303
|
+
code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'rename_failed',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @param {string} relPath
|
|
310
|
+
* @param {{ expectedVersion?: string|null, recursive?: boolean }} [opts]
|
|
311
|
+
*/
|
|
312
|
+
function remove(relPath, opts = {}) {
|
|
313
|
+
const gate = contain(relPath);
|
|
314
|
+
if (!gate.ok) return gate;
|
|
315
|
+
if (!fs.existsSync(gate.abs)) {
|
|
316
|
+
return { ok: false, error: 'not_found', code: 'ENOENT', path: gate.rel };
|
|
317
|
+
}
|
|
318
|
+
const st = fs.lstatSync(gate.abs);
|
|
319
|
+
if (st.isFile() && opts.expectedVersion) {
|
|
320
|
+
const current = hashBytes(fs.readFileSync(gate.abs));
|
|
321
|
+
if (current !== opts.expectedVersion) {
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
error: 'version_conflict',
|
|
325
|
+
code: 'version_conflict',
|
|
326
|
+
expected: opts.expectedVersion,
|
|
327
|
+
actual: current,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
if (st.isDirectory()) {
|
|
333
|
+
fs.rmSync(gate.abs, { recursive: Boolean(opts.recursive), force: false });
|
|
334
|
+
} else {
|
|
335
|
+
fs.unlinkSync(gate.abs);
|
|
336
|
+
}
|
|
337
|
+
markRepositoryStale(resolvedRoot, { paths: [gate.rel], reason: 'remove' });
|
|
338
|
+
return { ok: true, path: gate.rel, engine: LOCAL_FS_ENGINE };
|
|
339
|
+
} catch (err) {
|
|
340
|
+
return {
|
|
341
|
+
ok: false,
|
|
342
|
+
error: err instanceof Error ? err.message : String(err),
|
|
343
|
+
code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'remove_failed',
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {string} relPath
|
|
350
|
+
*/
|
|
351
|
+
function mkdir(relPath) {
|
|
352
|
+
const gate = contain(relPath);
|
|
353
|
+
if (!gate.ok) return gate;
|
|
354
|
+
try {
|
|
355
|
+
fs.mkdirSync(gate.abs, { recursive: true });
|
|
356
|
+
return { ok: true, path: gate.rel, engine: LOCAL_FS_ENGINE };
|
|
357
|
+
} catch (err) {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
error: err instanceof Error ? err.message : String(err),
|
|
361
|
+
code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'mkdir_failed',
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
root: resolvedRoot,
|
|
368
|
+
engine: LOCAL_FS_ENGINE,
|
|
369
|
+
list,
|
|
370
|
+
stat,
|
|
371
|
+
read,
|
|
372
|
+
write,
|
|
373
|
+
create,
|
|
374
|
+
rename,
|
|
375
|
+
remove,
|
|
376
|
+
mkdir,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Disposable scratch root under os.tmpdir for tests.
|
|
382
|
+
*/
|
|
383
|
+
export function createTempWorkspaceRoot(prefix = 'agentsam-fs-') {
|
|
384
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
385
|
+
}
|