@inneranimalmedia/agentsam-sdk 2.1.0 → 2.2.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/README.md +27 -15
- package/docs/CAPABILITIES.md +93 -0
- package/docs/RELEASES.md +2 -1
- package/docs/portable-knowledge.md +4 -4
- package/package.json +7 -1
- package/packages/identity/package.json +1 -1
- package/packages/identity/src/frontend/auth-portal/README.md +1 -1
- package/packages/identity/src/index.js +2 -0
- package/protocol/capabilities/capability-manifest.schema.json +36 -0
- package/protocol/capabilities/manifest.json +179 -0
- package/protocol/capabilities/repository-audit-input.schema.json +14 -0
- package/protocol/capabilities/repository-audit.schema.json +30 -0
- package/protocol/capabilities/repository-snapshot-input.schema.json +11 -0
- package/protocol/capabilities/repository-snapshot.schema.json +20 -0
- package/protocol/knowledge/chunk.schema.json +15 -73
- package/protocol/knowledge/document.schema.json +10 -48
- package/protocol/knowledge/index-config.schema.json +2 -2
- package/protocol/knowledge/repository.schema.json +11 -52
- package/protocol/knowledge/retrieval-query.schema.json +13 -63
- package/protocol/knowledge/source.schema.json +9 -43
- package/protocol/presets/catalog.json +48 -0
- package/python/agentsam_sdk/knowledge/models.py +19 -7
- package/python/agentsam_sdk/repository/__main__.py +2 -2
- package/python/tests/test_knowledge_models.py +6 -2
- package/src/agent/capability-adapter.js +50 -0
- package/src/agent/index.js +2 -0
- package/src/agent/repository-audit.js +188 -0
- package/src/capabilities/index.js +7 -0
- package/src/capabilities/manifest.js +22 -0
- package/src/capabilities/repository-snapshot.js +180 -0
- package/src/cli.js +53 -1
- package/src/commands/deploy.js +0 -1
- package/src/commands/knowledge.js +5 -6
- package/src/commands/product.js +119 -0
- package/src/index.js +9 -0
- package/src/knowledge/config.js +12 -6
- package/src/knowledge/contracts.js +1 -1
- package/src/knowledge/engine.js +1 -1
- package/src/knowledge/service/server.js +2 -2
- package/src/lib/git-context.js +3 -1
- package/src/presets/index.js +20 -0
- package/src/repository/index.js +4 -0
- package/test/agent-capabilities.test.mjs +67 -0
- package/test/capabilities.test.mjs +84 -0
- package/test/portable-context.test.mjs +11 -1
package/src/knowledge/config.js
CHANGED
|
@@ -37,7 +37,8 @@ export function validateConfig(input) {
|
|
|
37
37
|
allowed(c.embedding, ['provider', 'model', 'revision', 'dimensions', 'parameters'], 'embedding');
|
|
38
38
|
allowed(c.storage, ['driver', 'connection_env'], 'storage');
|
|
39
39
|
if (c.version !== 1) throw new Error('Unsupported knowledge configuration version.');
|
|
40
|
-
|
|
40
|
+
if (typeof c.repository_id !== 'string' || !c.repository_id.trim()) throw new Error('repository_id is required.');
|
|
41
|
+
if (c.workspace_id != null && (typeof c.workspace_id !== 'string' || !c.workspace_id.trim())) throw new Error('workspace_id must be a non-empty string when present.');
|
|
41
42
|
if (typeof c.scope?.name !== 'string' || !c.scope.name.trim() || !Array.isArray(c.scope.include) || !c.scope.include.length) throw new Error('A named scope with at least one include path is required.');
|
|
42
43
|
c.scope.include = [...new Set(c.scope.include.map(relativeSelection))].sort();
|
|
43
44
|
c.scope.exclude = [...new Set((c.scope.exclude || []).map(relativeSelection))].sort();
|
|
@@ -49,10 +50,9 @@ export function validateConfig(input) {
|
|
|
49
50
|
if (c.storage.driver === 'postgres' && !/^[A-Z_][A-Z0-9_]*$/.test(c.storage.connection_env || '')) throw new Error('Postgres requires a connection_env name, never a connection string in config.');
|
|
50
51
|
return c;
|
|
51
52
|
}
|
|
52
|
-
export function defaultConfig({ include = ['.'], exclude = [], scope = 'default',
|
|
53
|
+
export function defaultConfig({ include = ['.'], exclude = [], scope = 'default', target = 'local', dimensions = 768 } = {}) {
|
|
53
54
|
if (!['local', 'production'].includes(target)) throw new Error('target must be local or production.');
|
|
54
|
-
|
|
55
|
-
return validateConfig({ version: 1, repository_id: randomUUID(), workspace_id: workspace,
|
|
55
|
+
return validateConfig({ version: 1, repository_id: randomUUID(),
|
|
56
56
|
scope: { name: scope, include, exclude }, chunking: { max_chars: 4000 },
|
|
57
57
|
embedding: { provider: 'gemini', model: 'gemini-embedding-2', revision: '1', dimensions, parameters: { task: 'code retrieval' } },
|
|
58
58
|
storage: target === 'local' ? { driver: 'sqlite' } : { driver: 'postgres', connection_env: 'AGENTSAM_DATABASE_URL' } });
|
|
@@ -65,5 +65,11 @@ export function initRepository(root, options = {}) {
|
|
|
65
65
|
fs.writeFileSync(target, JSON.stringify(config, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
66
66
|
return config;
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// New portable configs are repository-scoped. Legacy configs that still carry
|
|
69
|
+
// workspace_id retain their original namespace so existing local generations stay readable.
|
|
70
|
+
export const scopeKey = config => config.workspace_id
|
|
71
|
+
? fingerprint([config.workspace_id, config.repository_id, config.scope.name])
|
|
72
|
+
: fingerprint([config.repository_id, config.scope.name]);
|
|
73
|
+
export const cacheNamespace = config => config.workspace_id
|
|
74
|
+
? fingerprint([config.workspace_id, config.repository_id])
|
|
75
|
+
: fingerprint([config.repository_id]);
|
|
@@ -8,7 +8,7 @@ export const KNOWLEDGE_OPERATIONS = Object.freeze({
|
|
|
8
8
|
export function assertRetrievalQuery(value) {
|
|
9
9
|
if (!value || typeof value !== "object") throw new TypeError("RetrievalQuery must be an object");
|
|
10
10
|
if (!String(value.text || "").trim()) throw new TypeError("RetrievalQuery.text is required");
|
|
11
|
-
if (!String(value.workspace_id
|
|
11
|
+
if (value.workspace_id != null && !String(value.workspace_id).trim()) throw new TypeError("RetrievalQuery.workspace_id must be non-empty when present");
|
|
12
12
|
const topK = value.top_k ?? 12;
|
|
13
13
|
if (!Number.isInteger(topK) || topK < 1 || topK > 100) throw new RangeError("top_k must be 1..100");
|
|
14
14
|
if (!Number.isInteger(value.token_budget ?? 8000) || (value.token_budget ?? 8000) < 256) throw new RangeError("token_budget must be at least 256");
|
package/src/knowledge/engine.js
CHANGED
|
@@ -113,6 +113,6 @@ export async function retrieve({ store, config: input, text, semantic = false, e
|
|
|
113
113
|
metadata: { generation_id: generation.id, symbol: hit.symbol, content_hash: hit.content_hash, freshness: 'working tree not checked' } }); estimatedTokens += tokens;
|
|
114
114
|
if (hits.length === topK) break;
|
|
115
115
|
}
|
|
116
|
-
return createContextPack({ queryId: randomUUID(), query: { text,
|
|
116
|
+
return createContextPack({ queryId: randomUUID(), query: { text, top_k: topK, token_budget: tokenBudget }, hits,
|
|
117
117
|
diagnostics: { generation_id: generation.id, source_hash: generation.source_hash, scope: generation.config.scope, mode: semantic ? 'semantic-exact' : 'lexical', freshness: 'snapshot; working tree not checked' } });
|
|
118
118
|
}
|
|
@@ -100,7 +100,7 @@ export async function startKnowledgeService({ stateDir, repositories, token, por
|
|
|
100
100
|
});
|
|
101
101
|
// Re-resolve registry on replay. Removed repositories do not get resumed.
|
|
102
102
|
const payload = JSON.parse(row.payload), registered = repositories[payload.request.repository];
|
|
103
|
-
if (!registered || registered.config.repository_id !== payload.config.repository_id ||
|
|
103
|
+
if (!registered || registered.config.repository_id !== payload.config.repository_id || fingerprint(registered.config.scope) !== payload.registered_scope || (!allowEmbeddings && ((payload.request.embed && payload.request.operation !== 'plan') || payload.request.semantic))) {
|
|
104
104
|
response = { ok: false, error: 'Repository registration changed; resubmit this job.' }; child.kill(); return;
|
|
105
105
|
}
|
|
106
106
|
child.send({ ...payload, root: registered.root, filename: path.join(stateDir, 'knowledge.sqlite'), maxFiles });
|
|
@@ -124,7 +124,7 @@ export async function startKnowledgeService({ stateDir, repositories, token, por
|
|
|
124
124
|
payload.registered_scope = fingerprint(repositories[payload.request.repository].config.scope);
|
|
125
125
|
const key = req.headers['idempotency-key'];
|
|
126
126
|
if (key !== undefined && (typeof key !== 'string' || !/^[\w:.-]{1,128}$/.test(key))) throw fail(400, 'Invalid Idempotency-Key.');
|
|
127
|
-
const digest = fingerprint(payload), idem = key ? fingerprint([payload.config.
|
|
127
|
+
const digest = fingerprint(payload), idem = key ? fingerprint([payload.config.repository_id, payload.registered_scope, key]) : null;
|
|
128
128
|
const previous = idem && db.prepare('SELECT * FROM jobs WHERE idem=?').get(idem);
|
|
129
129
|
if (previous) {
|
|
130
130
|
if (previous.digest !== digest) throw fail(409, 'Idempotency-Key already used for a different job.');
|
package/src/lib/git-context.js
CHANGED
|
@@ -66,7 +66,9 @@ export function resolveGitContext(options = {}) {
|
|
|
66
66
|
? preferredRemote
|
|
67
67
|
: availableRemotes[0] || preferredRemote;
|
|
68
68
|
const remoteUrl = runGit(root, ['remote', 'get-url', remote], { required: false });
|
|
69
|
-
|
|
69
|
+
// A newly initialized repository can have an unborn HEAD. Repository identity
|
|
70
|
+
// and local inspection must still work before the first commit.
|
|
71
|
+
const revisionSha = runGit(root, ['rev-parse', 'HEAD'], { required: false }) || null;
|
|
70
72
|
const branch = runGit(root, ['branch', '--show-current'], { required: false }) || null;
|
|
71
73
|
const status = runGit(root, ['status', '--porcelain=v1'], { required: false });
|
|
72
74
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import catalog from '../../protocol/presets/catalog.json' with { type: 'json' };
|
|
2
|
+
|
|
3
|
+
export const PRESET_CATALOG_VERSION = catalog.schema_version;
|
|
4
|
+
|
|
5
|
+
export function getPresetCatalog() { return structuredClone(catalog); }
|
|
6
|
+
export function listPresets() { return Object.values(catalog.presets).map((row) => structuredClone(row)); }
|
|
7
|
+
export function getPreset(id) {
|
|
8
|
+
const row = catalog.presets[String(id || '').trim().toLowerCase()];
|
|
9
|
+
return row ? structuredClone(row) : null;
|
|
10
|
+
}
|
|
11
|
+
export function resolvePreset(id = 'fullstack') {
|
|
12
|
+
const preset = getPreset(id);
|
|
13
|
+
if (!preset) throw new Error(`unknown_preset:${id}; expected ${Object.keys(catalog.presets).join(',')}`);
|
|
14
|
+
return preset;
|
|
15
|
+
}
|
|
16
|
+
export function listAddons() { return Object.values(catalog.addons).map((row) => structuredClone(row)); }
|
|
17
|
+
export function getAddon(id) {
|
|
18
|
+
const row = catalog.addons[String(id || '').trim().toLowerCase()];
|
|
19
|
+
return row ? structuredClone(row) : null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { repositorySnapshot } from '../capabilities/repository-snapshot.js';
|
|
2
|
+
export { buildMerkleTree, diffTrees, readSnapshot, saveSnapshot, validateSnapshot } from '../lib/merkle/index.js';
|
|
3
|
+
export { resolveGitContext, tryResolveGitContext, normalizeGitRemote } from '../lib/git-context.js';
|
|
4
|
+
export { buildRetrievalPlan, createContextPack } from '../knowledge/index.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createCapabilityAdapter } from '../src/agent/index.js';
|
|
4
|
+
import { buildRepositoryAuditPacket, runRepositoryAudit } from '../src/agent/repository-audit.js';
|
|
5
|
+
|
|
6
|
+
const snapshot = {
|
|
7
|
+
schema_version: 1,
|
|
8
|
+
capability: 'repository.snapshot',
|
|
9
|
+
snapshot_id: 'rsnap_0123456789abcdef01234567',
|
|
10
|
+
content_hash: `sha256:${'a'.repeat(64)}`,
|
|
11
|
+
repository: { repository_id: 'github:owner/repo', provider: 'github', full_name: 'owner/repo', branch: 'main', revision_sha: 'b'.repeat(40), dirty: false },
|
|
12
|
+
tree: { merkle_root: `sha256:${'c'.repeat(64)}`, stats: { files: 10 } },
|
|
13
|
+
intelligence: {
|
|
14
|
+
summary: { file_count: 10, total_lines: 500 },
|
|
15
|
+
languages: [{ language: 'JavaScript', files: 7 }],
|
|
16
|
+
manifests: [{ path: 'package.json', kind: 'node' }],
|
|
17
|
+
top_level: [{ path: 'src', files: 7 }],
|
|
18
|
+
pressure_points: [{ path: 'src', pressure_score: 70 }],
|
|
19
|
+
},
|
|
20
|
+
packages: [{ path: 'package.json', name: 'demo', version: '1.0.0' }],
|
|
21
|
+
knowledge: { configured: false },
|
|
22
|
+
deploy: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
test('repository audit packet is bounded read-only evidence', () => {
|
|
26
|
+
const packet = buildRepositoryAuditPacket({ snapshot, focus: ['packages'], evidenceBudget: 1000 });
|
|
27
|
+
assert.equal(packet.primitive, 'repository.audit');
|
|
28
|
+
assert.equal(packet.rules.read_only, true);
|
|
29
|
+
assert.equal(packet.rules.may_edit, false);
|
|
30
|
+
assert.equal(packet.snapshot_id, snapshot.snapshot_id);
|
|
31
|
+
assert.ok(JSON.stringify(packet.evidence).length <= 4000);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('repository audit uses injected reasoning and validates read-only output', async () => {
|
|
35
|
+
const audit = await runRepositoryAudit({
|
|
36
|
+
snapshot,
|
|
37
|
+
reasoner: async packet => ({
|
|
38
|
+
summary: `Audited ${packet.evidence.repository.full_name}`,
|
|
39
|
+
packages: packet.evidence.packages,
|
|
40
|
+
notable_combinations: ['snapshot + audit'],
|
|
41
|
+
findings: [{ severity: 'low', finding: 'example' }],
|
|
42
|
+
recommended_routes: ['inspect capability registry'],
|
|
43
|
+
evidence_refs: [packet.evidence_index.packages],
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
assert.equal(audit.primitive, 'repository.audit');
|
|
47
|
+
assert.equal(audit.summary, 'Audited owner/repo');
|
|
48
|
+
assert.equal(audit.packages.length, 1);
|
|
49
|
+
assert.deepEqual(audit.commands, []);
|
|
50
|
+
|
|
51
|
+
await assert.rejects(() => runRepositoryAudit({ snapshot, reasoner: async () => ({ summary: 'bad', jobs: [] }) }), /mutation field: jobs/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('agent capability adapter exposes only executable tools by default', async () => {
|
|
55
|
+
const adapter = createCapabilityAdapter({
|
|
56
|
+
reasoner: async packet => ({ summary: 'ok', evidence_refs: [packet.evidence_index.summary] }),
|
|
57
|
+
handlers: { 'knowledge.search': async input => ({ query: input.text, hits: [] }) },
|
|
58
|
+
});
|
|
59
|
+
const names = adapter.toolDescriptors().map(row => row.name);
|
|
60
|
+
assert.ok(names.includes('repository.snapshot'));
|
|
61
|
+
assert.ok(names.includes('repository.audit'));
|
|
62
|
+
assert.ok(names.includes('knowledge.search'));
|
|
63
|
+
assert.equal(names.includes('knowledge.index'), false);
|
|
64
|
+
const result = await adapter.invoke('knowledge.search', { text: 'hello' });
|
|
65
|
+
assert.equal(result.capability_id, 'knowledge.search');
|
|
66
|
+
assert.deepEqual(result.result.hits, []);
|
|
67
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { getCapability, getCapabilityManifest, repositorySnapshot } from '../src/capabilities/index.js';
|
|
8
|
+
import { getPreset, resolvePreset } from '../src/presets/index.js';
|
|
9
|
+
|
|
10
|
+
const CLI = path.resolve('src/cli.js');
|
|
11
|
+
|
|
12
|
+
function git(cwd, args) { return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); }
|
|
13
|
+
|
|
14
|
+
function fixture() {
|
|
15
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-capability-'));
|
|
16
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'snapshot-fixture', version: '1.0.0', type: 'module' }, null, 2));
|
|
17
|
+
fs.mkdirSync(path.join(root, 'src'));
|
|
18
|
+
fs.writeFileSync(path.join(root, 'src', 'index.js'), 'export const value = 1;\n');
|
|
19
|
+
git(root, ['init', '-q']);
|
|
20
|
+
git(root, ['config', 'user.email', 'test@example.com']);
|
|
21
|
+
git(root, ['config', 'user.name', 'AgentSam Test']);
|
|
22
|
+
git(root, ['add', '.']);
|
|
23
|
+
git(root, ['commit', '-qm', 'fixture']);
|
|
24
|
+
return root;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('capability manifest exposes deterministic primitives without requiring a model', () => {
|
|
28
|
+
const manifest = getCapabilityManifest();
|
|
29
|
+
assert.equal(manifest.schema_version, 1);
|
|
30
|
+
assert.equal(getCapability('repository.snapshot').model_required, false);
|
|
31
|
+
assert.equal(getCapability('repository.snapshot').side_effects, 'none');
|
|
32
|
+
assert.equal(getCapability('knowledge.index').deterministic, true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('presets resolve to explicit feature/capability selections', () => {
|
|
36
|
+
assert.equal(getPreset('cms').lane, 'cms');
|
|
37
|
+
const preset = resolvePreset('prototype');
|
|
38
|
+
assert.deepEqual(preset.features, ['agent']);
|
|
39
|
+
assert.throws(() => resolvePreset('nope'), /unknown_preset/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('repository.snapshot composes deterministic evidence and content addressing', async t => {
|
|
43
|
+
const root = fixture();
|
|
44
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
45
|
+
const one = await repositorySnapshot({ cwd: root, churnDays: 30 });
|
|
46
|
+
const two = await repositorySnapshot({ cwd: root, churnDays: 30 });
|
|
47
|
+
assert.equal(one.capability, 'repository.snapshot');
|
|
48
|
+
assert.match(one.snapshot_id, /^rsnap_[a-f0-9]{24}$/);
|
|
49
|
+
assert.match(one.content_hash, /^sha256:[a-f0-9]{64}$/);
|
|
50
|
+
assert.equal(one.content_hash, two.content_hash);
|
|
51
|
+
assert.equal(one.snapshot_id, two.snapshot_id);
|
|
52
|
+
assert.equal(one.repository.revision_sha, git(root, ['rev-parse', 'HEAD']));
|
|
53
|
+
assert.ok(one.tree.merkle_root);
|
|
54
|
+
assert.ok(one.intelligence.summary.file_count >= 2);
|
|
55
|
+
assert.equal(one.packages[0].name, 'snapshot-fixture');
|
|
56
|
+
assert.equal(one.knowledge.configured, false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('product UX creates a preset project, adds a feature, and inspects before first commit', t => {
|
|
60
|
+
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-product-'));
|
|
61
|
+
t.after(() => fs.rmSync(parent, { recursive: true, force: true }));
|
|
62
|
+
execFileSync(process.execPath, [CLI, 'create', 'demo', '--preset', 'cms', '--target', 'local'], {
|
|
63
|
+
cwd: parent,
|
|
64
|
+
stdio: 'pipe',
|
|
65
|
+
});
|
|
66
|
+
const project = path.join(parent, 'demo');
|
|
67
|
+
const config = JSON.parse(fs.readFileSync(path.join(project, '.agentsam', 'config.json'), 'utf8'));
|
|
68
|
+
assert.equal(config.preset, 'cms');
|
|
69
|
+
assert.equal(config.lane, 'cms');
|
|
70
|
+
assert.deepEqual(config.features, ['cms', 'knowledge']);
|
|
71
|
+
assert.ok(config.capabilities.includes('repository.snapshot'));
|
|
72
|
+
|
|
73
|
+
execFileSync(process.execPath, [CLI, 'add', 'knowledge', '--cwd', project, '--json'], { stdio: 'pipe' });
|
|
74
|
+
const features = JSON.parse(fs.readFileSync(path.join(project, '.agentsam', 'features.json'), 'utf8'));
|
|
75
|
+
assert.equal(features.features.knowledge.selected, true);
|
|
76
|
+
|
|
77
|
+
const snapshot = JSON.parse(execFileSync(process.execPath, [CLI, 'inspect', '--cwd', project, '--json'], {
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
+
}));
|
|
81
|
+
assert.equal(snapshot.capability, 'repository.snapshot');
|
|
82
|
+
assert.equal(snapshot.repository.revision_sha, null);
|
|
83
|
+
assert.ok(snapshot.tree.stats.files > 0);
|
|
84
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { mkdtempSync, realpathSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
|
|
@@ -39,6 +39,16 @@ test('resolveGitContext derives repository identity from Git without user/worksp
|
|
|
39
39
|
assert.equal('userId' in ctx, false);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
test('resolveGitContext supports an initialized repository before its first commit', () => {
|
|
43
|
+
const root = mkdtempSync(join(tmpdir(), 'agentsam-sdk-unborn-'));
|
|
44
|
+
execFileSync('git', ['init'], { cwd: root, stdio: 'ignore' });
|
|
45
|
+
writeFileSync(join(root, 'README.md'), '# unborn\n');
|
|
46
|
+
const ctx = resolveGitContext({ cwd: root });
|
|
47
|
+
assert.equal(ctx.root, realpathSync(root));
|
|
48
|
+
assert.equal(ctx.revisionSha, null);
|
|
49
|
+
assert.equal(ctx.dirty, true);
|
|
50
|
+
});
|
|
51
|
+
|
|
42
52
|
test('bridge headers authenticate only the machine principal', () => {
|
|
43
53
|
const headers = buildBridgeHeaders({
|
|
44
54
|
env: {
|