@inneranimalmedia/agentsam-sdk 2.5.0 → 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 +12 -8
- package/bin/agentsam +2 -0
- package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
- package/docs/CLI_SHELL.md +163 -53
- package/docs/RELEASES.md +16 -7
- package/package.json +20 -8
- 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 +2 -2
- package/packages/identity/src/contracts/auth-config.js +18 -7
- package/packages/identity/tests/auth-config.test.mjs +9 -5
- package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
- 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 +47 -0
- package/protocol/context/context-budget.schema.json +10 -15
- package/protocol/context/context-item.schema.json +4 -5
- package/protocol/context/resolved-context-pack.schema.json +19 -14
- package/protocol/models/README.md +373 -0
- package/protocol/models/model-inventory-v2.schema.json +212 -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/catalog.json +18 -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/cli.js +98 -28
- 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/deploy.js +39 -3
- package/src/commands/eval.js +63 -0
- package/src/commands/interactive.js +2 -5
- package/src/commands/models.js +85 -40
- package/src/commands/preferences.js +101 -59
- package/src/commands/resume.js +67 -0
- package/src/commands/security.js +5 -3
- package/src/commands/shell.js +370 -109
- package/src/commands/tunnel.js +2 -2
- package/src/commands/whoami.js +86 -0
- package/src/context/budget.js +68 -6
- package/src/context/index.js +3 -1
- package/src/context/rehydrate.js +35 -0
- package/src/context/resolve.js +44 -12
- 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 +55 -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/cli-preferences.js +28 -24
- 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/detect-context.js +2 -2
- package/src/lib/execution-approvals.js +59 -0
- package/src/lib/local-sessions.js +127 -0
- package/src/lib/provider-credentials.js +83 -0
- 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 -16
- 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/process.js +35 -9
- 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 +1 -0
- package/src/ui/boot.js +15 -17
- package/test/account-session.test.mjs +36 -0
- package/test/cli-preferences.test.mjs +26 -5
- package/test/cloudflare-connector.test.mjs +96 -0
- package/test/cloudflare-runtime.test.mjs +75 -0
- package/test/context.test.mjs +61 -12
- 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/local-sessions.test.mjs +42 -0
- package/test/local-studio-deploy.test.mjs +83 -0
- package/test/model-catalog.test.mjs +43 -0
- package/test/models.test.mjs +30 -16
- package/test/npm10-lock.test.mjs +29 -0
- package/test/openai-responses.test.mjs +95 -0
- package/test/provider-credentials.test.mjs +52 -0
- package/test/rehydrate.test.mjs +25 -0
- package/test/release-hygiene.test.mjs +4 -4
- package/test/responses-runner.test.mjs +148 -0
- package/test/shell.test.mjs +47 -20
- package/test/smoke.mjs +4 -1
- package/test/telemetry.test.mjs +79 -0
- package/test/tools-search.test.mjs +14 -1
- package/test/whoami-resume.test.mjs +56 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const DEFAULT_HEALTH_ORIGIN = 'https://agentsam.inneranimalmedia.com';
|
|
2
|
+
export const HEALTH_USER_AGENT = 'AgentSam-deploy-health/1';
|
|
3
|
+
|
|
4
|
+
export function parseWranglerVersionId(output = '') {
|
|
5
|
+
const text = String(output || '');
|
|
6
|
+
const m = text.match(/Current Version ID:\s*([0-9a-f-]{36})/i)
|
|
7
|
+
|| text.match(/Version ID:\s*([0-9a-f-]{36})/i);
|
|
8
|
+
return m ? m[1] : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveHealthOrigin({ env = process.env, wranglerConfigText = '' } = {}) {
|
|
12
|
+
const fromEnv = String(env.PRODUCT_HOST || env.AGENTSAM_HEALTH_ORIGIN || '').trim();
|
|
13
|
+
if (fromEnv) return fromEnv.replace(/\/+$/, '');
|
|
14
|
+
const m = String(wranglerConfigText || '').match(/"pattern"\s*:\s*"([^"]+)"/);
|
|
15
|
+
if (m) {
|
|
16
|
+
const host = m[1].trim();
|
|
17
|
+
if (host.startsWith('http://') || host.startsWith('https://')) return host.replace(/\/+$/, '');
|
|
18
|
+
return `https://${host.replace(/\/+$/, '')}`;
|
|
19
|
+
}
|
|
20
|
+
return DEFAULT_HEALTH_ORIGIN;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function probeDeployHealth(origin, { fetchImpl = globalThis.fetch, paths = ['/health', '/'] } = {}) {
|
|
24
|
+
const results = {};
|
|
25
|
+
for (const p of paths) {
|
|
26
|
+
const url = `${String(origin).replace(/\/+$/, '')}${p}`;
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetchImpl(url, {
|
|
29
|
+
redirect: 'manual',
|
|
30
|
+
headers: {
|
|
31
|
+
'User-Agent': HEALTH_USER_AGENT,
|
|
32
|
+
Accept: 'application/json,text/html,*/*',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
let body = null;
|
|
36
|
+
if (p === '/health') {
|
|
37
|
+
try { body = await res.json(); } catch { body = null; }
|
|
38
|
+
}
|
|
39
|
+
const ok = res.status >= 200 && res.status < 400;
|
|
40
|
+
results[p] = {
|
|
41
|
+
status: res.status,
|
|
42
|
+
ok,
|
|
43
|
+
...(body && typeof body === 'object' ? {
|
|
44
|
+
appOk: body.ok === true,
|
|
45
|
+
cloudflareConfigured: Boolean(body?.connections?.cloudflare?.configured),
|
|
46
|
+
} : {}),
|
|
47
|
+
};
|
|
48
|
+
} catch (err) {
|
|
49
|
+
results[p] = { status: 0, ok: false, error: err.message };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
origin,
|
|
54
|
+
results,
|
|
55
|
+
ok: Object.values(results).every((r) => r.ok),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { assertProductionDeployAllowed, inspectDeployGit } from './git-guard.js';
|
|
6
|
+
import { assertNoDeploySecrets } from './secret-scan.js';
|
|
7
|
+
import { parseWranglerVersionId, probeDeployHealth, resolveHealthOrigin } from './health.js';
|
|
8
|
+
|
|
9
|
+
export const LOCAL_STUDIO_REL = 'apps/local-studio';
|
|
10
|
+
export const WRANGLER_CONFIG_REL = 'backend/wrangler.jsonc';
|
|
11
|
+
export const DEPLOY_INPUT_GLOBS = Object.freeze([
|
|
12
|
+
'apps/local-studio',
|
|
13
|
+
'packages/agentsam-contracts',
|
|
14
|
+
'packages/agentsam-workbench',
|
|
15
|
+
'packages/connectors/cloudflare',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function findRepoRoot(start = process.cwd()) {
|
|
19
|
+
let dir = path.resolve(start);
|
|
20
|
+
for (;;) {
|
|
21
|
+
if (fs.existsSync(path.join(dir, 'apps/local-studio/backend/wrangler.jsonc'))) return dir;
|
|
22
|
+
if (fs.existsSync(path.join(dir, 'backend/wrangler.jsonc')) && path.basename(dir) === 'local-studio') {
|
|
23
|
+
return path.dirname(path.dirname(dir));
|
|
24
|
+
}
|
|
25
|
+
const parent = path.dirname(dir);
|
|
26
|
+
if (parent === dir) return start;
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveLocalStudioDeployable(cwd = process.cwd()) {
|
|
32
|
+
const repoRoot = findRepoRoot(cwd);
|
|
33
|
+
const appRoot = path.join(repoRoot, LOCAL_STUDIO_REL);
|
|
34
|
+
const wranglerConfig = path.join(appRoot, WRANGLER_CONFIG_REL);
|
|
35
|
+
if (!fs.existsSync(wranglerConfig)) {
|
|
36
|
+
throw new Error(`wrangler config missing: ${wranglerConfig}`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
repoRoot,
|
|
40
|
+
appRoot,
|
|
41
|
+
wranglerConfig,
|
|
42
|
+
wranglerArgs: ['deploy', '-c', WRANGLER_CONFIG_REL],
|
|
43
|
+
provider: 'cloudflare',
|
|
44
|
+
app: 'local-studio',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadOptionalCloudflareEnv(appRoot) {
|
|
49
|
+
const file = path.join(appRoot, '.env.cloudflare');
|
|
50
|
+
if (!fs.existsSync(file)) return { loaded: false, vars: {} };
|
|
51
|
+
const vars = {};
|
|
52
|
+
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
53
|
+
const trimmed = line.trim();
|
|
54
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
55
|
+
const eq = trimmed.indexOf('=');
|
|
56
|
+
if (eq < 1) continue;
|
|
57
|
+
const key = trimmed.slice(0, eq).trim();
|
|
58
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
59
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
60
|
+
value = value.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
vars[key] = value;
|
|
63
|
+
}
|
|
64
|
+
return { loaded: true, vars };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hashFile(file) {
|
|
68
|
+
const h = crypto.createHash('sha256');
|
|
69
|
+
h.update(fs.readFileSync(file));
|
|
70
|
+
return h.digest('hex');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function walkFiles(dir, acc = []) {
|
|
74
|
+
if (!fs.existsSync(dir)) return acc;
|
|
75
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
76
|
+
if (ent.name === 'node_modules' || ent.name === '.output' || ent.name === '.wrangler' || ent.name === '.git') continue;
|
|
77
|
+
const full = path.join(dir, ent.name);
|
|
78
|
+
if (ent.isDirectory()) walkFiles(full, acc);
|
|
79
|
+
else acc.push(full);
|
|
80
|
+
}
|
|
81
|
+
return acc;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function computeDeployFingerprint(repoRoot) {
|
|
85
|
+
const h = crypto.createHash('sha256');
|
|
86
|
+
const files = [];
|
|
87
|
+
for (const rel of DEPLOY_INPUT_GLOBS) {
|
|
88
|
+
walkFiles(path.join(repoRoot, rel), files);
|
|
89
|
+
}
|
|
90
|
+
files.sort();
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const rel = path.relative(repoRoot, file).split(path.sep).join('/');
|
|
93
|
+
h.update(rel);
|
|
94
|
+
h.update('\0');
|
|
95
|
+
h.update(hashFile(file));
|
|
96
|
+
h.update('\n');
|
|
97
|
+
}
|
|
98
|
+
return h.digest('hex');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function readBaselineFingerprint(appRoot) {
|
|
102
|
+
const receipt = path.join(appRoot, '.agentsam/deploy-merkle/latest.receipt.json');
|
|
103
|
+
if (!fs.existsSync(receipt)) return null;
|
|
104
|
+
try {
|
|
105
|
+
const json = JSON.parse(fs.readFileSync(receipt, 'utf8'));
|
|
106
|
+
return json.deployProjectionHash || json.fingerprint || null;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function writeDeployReceipt(appRoot, receipt) {
|
|
113
|
+
const dir = path.join(appRoot, '.agentsam/deploy-merkle');
|
|
114
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
115
|
+
const file = path.join(dir, receipt.promoted ? 'latest.receipt.json' : 'pending.receipt.json');
|
|
116
|
+
fs.writeFileSync(file, JSON.stringify(receipt, null, 2) + '\n');
|
|
117
|
+
return file;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function wranglerDeployCommand(target, { dryRun = false } = {}) {
|
|
121
|
+
const args = ['wrangler', ...target.wranglerArgs];
|
|
122
|
+
if (dryRun) args.push('--dry-run');
|
|
123
|
+
return { cwd: target.appRoot, args, bin: 'npx' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isLocalStudioCheckout(cwd = process.cwd()) {
|
|
127
|
+
try {
|
|
128
|
+
resolveLocalStudioDeployable(cwd);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function runLocalStudioDeploy({
|
|
136
|
+
cwd = process.cwd(),
|
|
137
|
+
dryRun = false,
|
|
138
|
+
planOnly = false,
|
|
139
|
+
skipBuild = false,
|
|
140
|
+
execute = true,
|
|
141
|
+
skipSecretScan = false,
|
|
142
|
+
probeHealth = null,
|
|
143
|
+
} = {}) {
|
|
144
|
+
const target = resolveLocalStudioDeployable(cwd);
|
|
145
|
+
if (path.resolve(cwd) === target.repoRoot && !target.wranglerConfig.endsWith(path.join('backend', 'wrangler.jsonc')) && !target.wranglerConfig.endsWith('backend/wrangler.jsonc')) {
|
|
146
|
+
throw new Error('refusing generic repo-root wrangler deploy');
|
|
147
|
+
}
|
|
148
|
+
const envFile = loadOptionalCloudflareEnv(target.appRoot);
|
|
149
|
+
const fingerprint = computeDeployFingerprint(target.repoRoot);
|
|
150
|
+
const baseline = readBaselineFingerprint(target.appRoot);
|
|
151
|
+
const gitInfo = inspectDeployGit(target.repoRoot);
|
|
152
|
+
const wranglerText = fs.readFileSync(target.wranglerConfig, 'utf8');
|
|
153
|
+
const healthOrigin = resolveHealthOrigin({ env: { ...process.env, ...envFile.vars }, wranglerConfigText: wranglerText });
|
|
154
|
+
const plan = {
|
|
155
|
+
provider: 'cloudflare',
|
|
156
|
+
app: 'local-studio',
|
|
157
|
+
wranglerConfig: path.relative(target.repoRoot, target.wranglerConfig).split(path.sep).join('/'),
|
|
158
|
+
wranglerArgs: target.wranglerArgs,
|
|
159
|
+
cwd: path.relative(target.repoRoot, target.appRoot).split(path.sep).join('/') || '.',
|
|
160
|
+
envLoaded: envFile.loaded,
|
|
161
|
+
fingerprint,
|
|
162
|
+
baseline,
|
|
163
|
+
skip: Boolean(baseline && baseline === fingerprint && !dryRun && !planOnly),
|
|
164
|
+
genericRootDeploy: false,
|
|
165
|
+
productionGitRequired: Boolean(execute && !dryRun && !planOnly),
|
|
166
|
+
healthOrigin,
|
|
167
|
+
git: {
|
|
168
|
+
branch: gitInfo.branch,
|
|
169
|
+
head: gitInfo.head,
|
|
170
|
+
originMain: gitInfo.originMain,
|
|
171
|
+
equal: Boolean(gitInfo.head && gitInfo.head === gitInfo.originMain),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
if (!skipSecretScan) {
|
|
175
|
+
plan.secretScan = assertNoDeploySecrets([
|
|
176
|
+
target.wranglerConfig,
|
|
177
|
+
path.join(target.appRoot, '.env.cloudflare.example'),
|
|
178
|
+
path.join(target.appRoot, 'backend/worker/index.js'),
|
|
179
|
+
path.join(target.repoRoot, 'packages/connectors/cloudflare/src/index.js'),
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
if (plan.skip) {
|
|
183
|
+
const receipt = {
|
|
184
|
+
ok: true,
|
|
185
|
+
skipped: true,
|
|
186
|
+
reason: 'unchanged_deploy_fingerprint',
|
|
187
|
+
deployProjectionHash: fingerprint,
|
|
188
|
+
promoted: false,
|
|
189
|
+
git: plan.git,
|
|
190
|
+
wranglerConfig: plan.wranglerConfig,
|
|
191
|
+
hostname: healthOrigin.replace(/^https?:\/\//, ''),
|
|
192
|
+
};
|
|
193
|
+
writeDeployReceipt(target.appRoot, receipt);
|
|
194
|
+
return { target, plan, receipt };
|
|
195
|
+
}
|
|
196
|
+
if (!execute || planOnly) {
|
|
197
|
+
return {
|
|
198
|
+
target,
|
|
199
|
+
plan,
|
|
200
|
+
receipt: {
|
|
201
|
+
ok: true,
|
|
202
|
+
skipped: false,
|
|
203
|
+
plan: true,
|
|
204
|
+
dryRun,
|
|
205
|
+
deployProjectionHash: fingerprint,
|
|
206
|
+
promoted: false,
|
|
207
|
+
git: plan.git,
|
|
208
|
+
wranglerConfig: plan.wranglerConfig,
|
|
209
|
+
hostname: healthOrigin.replace(/^https?:\/\//, ''),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!dryRun) {
|
|
215
|
+
plan.git = assertProductionDeployAllowed(target.repoRoot);
|
|
216
|
+
plan.git.equal = plan.git.head === plan.git.originMain;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const env = { ...process.env, ...envFile.vars };
|
|
220
|
+
const lock = spawnSync('npm', ['run', 'verify:npm10-lock'], { cwd: target.appRoot, env, encoding: 'utf8' });
|
|
221
|
+
if (lock.status !== 0) throw new Error(lock.stderr || lock.stdout || 'verify:npm10-lock failed');
|
|
222
|
+
if (!skipBuild) {
|
|
223
|
+
const build = spawnSync('npm', ['run', 'build'], { cwd: target.appRoot, env, encoding: 'utf8' });
|
|
224
|
+
if (build.status !== 0) throw new Error(build.stderr || build.stdout || 'build failed');
|
|
225
|
+
const verify = spawnSync('npm', ['run', 'cf:verify-output'], { cwd: target.appRoot, env, encoding: 'utf8' });
|
|
226
|
+
if (verify.status !== 0) throw new Error(verify.stderr || verify.stdout || 'cf:verify-output failed');
|
|
227
|
+
}
|
|
228
|
+
const cmd = wranglerDeployCommand(target, { dryRun });
|
|
229
|
+
const deployed = spawnSync(cmd.bin, cmd.args, { cwd: cmd.cwd, env, encoding: 'utf8' });
|
|
230
|
+
const wranglerOut = `${deployed.stdout || ''}\n${deployed.stderr || ''}`;
|
|
231
|
+
const workerVersion = parseWranglerVersionId(wranglerOut);
|
|
232
|
+
if (deployed.status !== 0) {
|
|
233
|
+
const receipt = {
|
|
234
|
+
ok: false,
|
|
235
|
+
skipped: false,
|
|
236
|
+
deployProjectionHash: fingerprint,
|
|
237
|
+
promoted: false,
|
|
238
|
+
git: plan.git,
|
|
239
|
+
workerVersion,
|
|
240
|
+
error: wranglerOut,
|
|
241
|
+
};
|
|
242
|
+
writeDeployReceipt(target.appRoot, receipt);
|
|
243
|
+
throw new Error(receipt.error || 'wrangler deploy failed');
|
|
244
|
+
}
|
|
245
|
+
let health = null;
|
|
246
|
+
if (!dryRun) {
|
|
247
|
+
const probe = probeHealth || probeDeployHealth;
|
|
248
|
+
health = await probe(healthOrigin);
|
|
249
|
+
if (!health.ok) {
|
|
250
|
+
const receipt = {
|
|
251
|
+
ok: false,
|
|
252
|
+
skipped: false,
|
|
253
|
+
deployProjectionHash: fingerprint,
|
|
254
|
+
promoted: false,
|
|
255
|
+
git: plan.git,
|
|
256
|
+
workerVersion,
|
|
257
|
+
hostname: healthOrigin.replace(/^https?:\/\//, ''),
|
|
258
|
+
health,
|
|
259
|
+
error: 'postdeploy_health_failed',
|
|
260
|
+
};
|
|
261
|
+
writeDeployReceipt(target.appRoot, receipt);
|
|
262
|
+
throw new Error('postdeploy_health_failed');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const receipt = {
|
|
266
|
+
ok: true,
|
|
267
|
+
skipped: false,
|
|
268
|
+
dryRun,
|
|
269
|
+
deployProjectionHash: fingerprint,
|
|
270
|
+
promoted: !dryRun,
|
|
271
|
+
provider: 'cloudflare',
|
|
272
|
+
app: 'local-studio',
|
|
273
|
+
wranglerConfig: plan.wranglerConfig,
|
|
274
|
+
git: plan.git,
|
|
275
|
+
worker: 'agentsam-sdk',
|
|
276
|
+
workerVersion,
|
|
277
|
+
hostname: healthOrigin.replace(/^https?:\/\//, ''),
|
|
278
|
+
health,
|
|
279
|
+
originMainEqual: Boolean(plan.git?.head && plan.git.head === plan.git.originMain),
|
|
280
|
+
};
|
|
281
|
+
writeDeployReceipt(target.appRoot, receipt);
|
|
282
|
+
return { target, plan, receipt };
|
|
283
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Scan deployable files for live secrets. Fixtures and .example placeholders are allowed. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
const FIXTURES = new Set([
|
|
5
|
+
'sillynotreal',
|
|
6
|
+
'sillynotreal-secret',
|
|
7
|
+
'CHANGEME',
|
|
8
|
+
'replace-me',
|
|
9
|
+
'your-token-here',
|
|
10
|
+
'***',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const LIVE_PATTERNS = [
|
|
14
|
+
{ re: /\bsk-[A-Za-z0-9]{20,}\b/g, label: 'openai-like' },
|
|
15
|
+
{ re: /\bxai-[A-Za-z0-9]{20,}\b/g, label: 'xai-like' },
|
|
16
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b/g, label: 'aws-like' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const ASSIGN_RE = /((?:CLOUDFLARE_OAUTH_CLIENT_SECRET|CLOUDFLARE_API_TOKEN|IAM_CLIENT_SECRET|VAULT_MASTER_KEY)\s*=\s*)([^\s#]+)/g;
|
|
20
|
+
|
|
21
|
+
function isExample(filename = '') {
|
|
22
|
+
return filename.endsWith('.example') || filename.includes('.env.cloudflare.example');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function scanTextForSecrets(text, { filename = '' } = {}) {
|
|
26
|
+
const findings = [];
|
|
27
|
+
const src = String(text || '');
|
|
28
|
+
for (const { re, label } of LIVE_PATTERNS) {
|
|
29
|
+
re.lastIndex = 0;
|
|
30
|
+
let m;
|
|
31
|
+
while ((m = re.exec(src))) {
|
|
32
|
+
if (FIXTURES.has(m[0])) continue;
|
|
33
|
+
findings.push({ filename, label, preview: `${m[0].slice(0, 5)}…` });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (isExample(filename)) return findings;
|
|
37
|
+
ASSIGN_RE.lastIndex = 0;
|
|
38
|
+
let m;
|
|
39
|
+
while ((m = ASSIGN_RE.exec(src))) {
|
|
40
|
+
const val = m[2].replace(/^[\'"]|[\'"]$/g, '');
|
|
41
|
+
if (!val || FIXTURES.has(val) || val.includes('your-') || val.length < 16) continue;
|
|
42
|
+
findings.push({ filename, label: 'assigned-secret', preview: `${m[1].trim()}[redacted]` });
|
|
43
|
+
}
|
|
44
|
+
return findings;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function scanFilesForSecrets(files = []) {
|
|
48
|
+
const findings = [];
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
if (!file || !fs.existsSync(file) || !fs.statSync(file).isFile()) continue;
|
|
51
|
+
findings.push(...scanTextForSecrets(fs.readFileSync(file, 'utf8'), { filename: file }));
|
|
52
|
+
}
|
|
53
|
+
return findings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function assertNoDeploySecrets(files = []) {
|
|
57
|
+
const findings = scanFilesForSecrets(files);
|
|
58
|
+
if (findings.length) {
|
|
59
|
+
const err = new Error('secret_scan_failed: ' + findings.map((f) => f.label).join(', '));
|
|
60
|
+
err.code = 'secret_scan_failed';
|
|
61
|
+
err.findings = findings;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, findings: [] };
|
|
65
|
+
}
|
|
@@ -5,7 +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 {
|
|
8
|
+
import { resolveAccountSdkKey } from './account-session.js';
|
|
9
9
|
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
11
|
|
|
@@ -221,7 +221,7 @@ async function detectIam(explicitToken = '') {
|
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
const sdkToken =
|
|
224
|
+
const sdkToken = resolveAccountSdkKey({ env: process.env, explicit: explicitToken }).value;
|
|
225
225
|
if (sdkToken.trim()) {
|
|
226
226
|
const probe = await probeSdkBearer(sdkToken);
|
|
227
227
|
if (probe.valid) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const SCHEMA = 'agentsam-execution-approvals-v1';
|
|
6
|
+
|
|
7
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
8
|
+
function homeDirectory(options = {}) { return path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir()); }
|
|
9
|
+
function approvalsPath(options = {}) { return path.join(homeDirectory(options), '.agentsam', 'execution-approvals.json'); }
|
|
10
|
+
|
|
11
|
+
function readStore(options = {}) {
|
|
12
|
+
const filename = approvalsPath(options);
|
|
13
|
+
if (!fs.existsSync(filename)) return { schema_version: SCHEMA, approvals: [] };
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
16
|
+
if (parsed?.schema_version !== SCHEMA || !Array.isArray(parsed.approvals)) return { schema_version: SCHEMA, approvals: [] };
|
|
17
|
+
return parsed;
|
|
18
|
+
} catch { return { schema_version: SCHEMA, approvals: [] }; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeStore(store, options = {}) {
|
|
22
|
+
const filename = approvalsPath(options);
|
|
23
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true, mode: 0o700 });
|
|
24
|
+
const temp = `${filename}.${process.pid}.tmp`;
|
|
25
|
+
fs.writeFileSync(temp, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
|
|
26
|
+
if (process.platform !== 'win32') {
|
|
27
|
+
try { fs.chmodSync(temp, 0o600); } catch { /* best effort */ }
|
|
28
|
+
}
|
|
29
|
+
fs.renameSync(temp, filename);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toolApprovalKey(capabilityId, input = {}) {
|
|
33
|
+
const id = clean(capabilityId);
|
|
34
|
+
const command = clean(input?.command);
|
|
35
|
+
if (id === 'cloudflare.wrangler.native' && /^[A-Za-z0-9._-]+$/.test(command)) return `${id}:${command}`;
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isExecutionApproved({ cwd, key }, options = {}) {
|
|
40
|
+
const root = path.resolve(clean(cwd) || process.cwd());
|
|
41
|
+
const target = clean(key);
|
|
42
|
+
if (!target) return false;
|
|
43
|
+
return readStore(options).approvals.some((row) => row?.cwd === root && row?.key === target);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function grantExecutionApproval({ cwd, key, label = '' }, options = {}) {
|
|
47
|
+
const root = path.resolve(clean(cwd) || process.cwd());
|
|
48
|
+
const target = clean(key);
|
|
49
|
+
if (!target) throw new Error('execution_approval_key_required');
|
|
50
|
+
const store = readStore(options);
|
|
51
|
+
const approvals = store.approvals.filter((row) => !(row?.cwd === root && row?.key === target));
|
|
52
|
+
approvals.push({ cwd: root, key: target, label: clean(label) || target, created_at: new Date().toISOString() });
|
|
53
|
+
writeStore({ schema_version: SCHEMA, approvals }, options);
|
|
54
|
+
return { cwd: root, key: target, persisted: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function listExecutionApprovals(options = {}) {
|
|
58
|
+
return readStore(options).approvals.map((row) => ({ ...row }));
|
|
59
|
+
}
|
|
@@ -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
|
+
}
|