Untested: add library write tools and served blob integration (#1793)
This commit is contained in:
1 parent
29455c0c5d
commit
cd2f50daec
4 files changed
+91
-2
No files matched your search
@@ -5,7 +5,7 @@ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync } from 'nod
|
|||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { connect } from 'node:net';
|
import { connect } from 'node:net';
|
||||||
import { startBroker } from '../src/runtime.mjs';
|
import { startBroker, createLibraryLookup } from '../src/runtime.mjs';
|
||||||
import { Store } from '../src/store.mjs';
|
import { Store } from '../src/store.mjs';
|
||||||
import { Broker } from '../src/broker.mjs';
|
import { Broker } from '../src/broker.mjs';
|
||||||
import { Client } from '../src/client.mjs';
|
import { Client } from '../src/client.mjs';
|
||||||
@@ -309,3 +309,24 @@ test('C2-02 mos-1520: failed notice insert logs metadata and returns outcome-unk
|
|||||||
assert.deepEqual(f.logs, [{ event: 'library-notice-store-failed', code: 'outcome-unknown', business: 'acme',
|
assert.deepEqual(f.logs, [{ event: 'library-notice-store-failed', code: 'outcome-unknown', business: 'acme',
|
||||||
instance: 'bean', op: 'write', commit: bare.remoteHead() }]);
|
instance: 'bean', op: 'write', commit: bare.remoteHead() }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('C2-02 + C2-05c: broker get blob feeds write/delete and new served lookups immediately', async t => {
|
||||||
|
const bare = await bareWorld(t), f = world(t, { projects: bare.context.business.projects });
|
||||||
|
f.library.lookup = createLibraryLookup({ dataRoot: bare.dataRoot,
|
||||||
|
sync: context => sync({ ...context, dataRoot: bare.dataRoot }, { credential: bare.credential, command: bare.command }) });
|
||||||
|
f.library.writeDoc = (context, options) => writeLibraryDoc({ ...context, dataRoot: bare.dataRoot },
|
||||||
|
{ ...options, credential: { ...options.credential, file: bare.file }, command: bare.command });
|
||||||
|
const query = { universe: 'harbor', path: PATH, scope: 'supporting' };
|
||||||
|
const before = await f.call('bean', 'library.get', query);
|
||||||
|
assert.equal(before.blob, git(bare.mirror, 'rev-parse', `served:${PATH}`));
|
||||||
|
const saved = await f.call('bean', 'library.doc.write', { ...args(), base: before.blob, content: 'new served text\n' });
|
||||||
|
const after = await f.call('bean', 'library.get', query);
|
||||||
|
assert.equal(after.commit, saved.commit);
|
||||||
|
assert.equal(after.blob, git(bare.mirror, 'rev-parse', `${saved.commit}:${PATH}`));
|
||||||
|
assert.notEqual(after.blob, before.blob);
|
||||||
|
assert.ok(after.lines.includes('new served text'));
|
||||||
|
assert.equal(await f.call('bean', 'library.get', { universe: 'harbor', path: PATH }), null);
|
||||||
|
await f.call('bean', 'library.doc.delete', { universe: 'harbor', path: PATH, base: after.blob });
|
||||||
|
assert.equal(await f.call('bean', 'library.get', query), null);
|
||||||
|
});
|
||||||
@@ -249,6 +249,19 @@ export function typedTools({ resolved, arbiter = false, tracker = false, project
|
|||||||
...base, book, scope: manuscriptScope, chapter: str("Chapter filename without extension"), from: line, to: line,
|
...base, book, scope: manuscriptScope, chapter: str("Chapter filename without extension"), from: line, to: line,
|
||||||
}, ["universe", "book"]));
|
}, ["universe", "book"]));
|
||||||
add("library_universe", "library.universe", null, "Read a universe's mapping at the served commit: books, types, playground prefixes, and hybrid sources/constraints/notes or draft metadata.", obj(base, ["universe"]));
|
add("library_universe", "library.universe", null, "Read a universe's mapping at the served commit: books, types, playground prefixes, and hybrid sources/constraints/notes or draft metadata.", obj(base, ["universe"]));
|
||||||
|
// C2-02: write tools carry only the note/base/content. The broker supplies the writer,
|
||||||
|
// credential and fixed commit message; a creative within-role grant is required.
|
||||||
|
if (CREATIVE_DEFINITIONS.includes(resolved.definition) && resolved.limits.authority.withinRole.includes("library.write")) {
|
||||||
|
const writeBase = {
|
||||||
|
universe,
|
||||||
|
path: str("A .md path under this universe's human-marked supporting folder"),
|
||||||
|
base: { type: ["string", "null"], pattern: "^[a-f0-9]{40}$", description: "The blob from library_get, or null to create; never the commit id" },
|
||||||
|
};
|
||||||
|
add("library_doc_write", "library.doc.write", "library.write", "Create or edit one supporting .md note using the blob from library_get. One commit by your identity, with a fixed human notice. A stale base refuses; agent requests retain the 64 KiB envelope limit.", obj({
|
||||||
|
...writeBase, content: str("UTF-8 note text without NUL; at most 1 MiB and bounded by the agent socket envelope"),
|
||||||
|
}, ["universe", "path", "base", "content"]));
|
||||||
|
add("library_doc_delete", "library.doc.delete", "library.write", "Delete one existing regular supporting .md note using the blob from library_get. One commit and a fixed human notice. A stale base or nonregular/missing target refuses.", obj(writeBase, ["universe", "path", "base"]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
add("read_spec", "spec.read", null, "Read a project's committed north-star spec and its file history. commit is an optional full reachable commit ID. The file commit, rather than HEAD, is what a decision cites. confirmed says the file commit was authored by the business's human.", obj({
|
add("read_spec", "spec.read", null, "Read a project's committed north-star spec and its file history. commit is an optional full reachable commit ID. The file commit, rather than HEAD, is what a decision cites. confirmed says the file commit was authored by the business's human.", obj({
|
||||||
project: str("Project ID from the business file"),
|
project: str("Project ID from the business file"),
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// Row 288 M3: creative write tools and independent closed-schema coverage.
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { typedTools, untypedActions } from '../src/tools.mjs';
|
||||||
|
import { resolvedFor } from './helpers.mjs';
|
||||||
|
|
||||||
|
const LOOKUPS = ['library_find', 'library_get', 'library_search', 'library_manuscript', 'library_universe'];
|
||||||
|
const WRITES = ['library_doc_write', 'library_doc_delete'];
|
||||||
|
const CREATIVE = ['companion', 'research', 'ideas', 'proofreader', 'fact-checker', 'continuity'];
|
||||||
|
const role = (t, definition) => resolvedFor(t, 'bean', doc => {
|
||||||
|
doc.roles.bean = { definition, vars: { harness: 'pi', model: 'claude-sonnet-5-5', thinking: 'high' } };
|
||||||
|
});
|
||||||
|
|
||||||
|
test('C2-02 M3: each creative role has eight tools, or ten with its web grant', t => {
|
||||||
|
for (const definition of CREATIVE) {
|
||||||
|
const resolved = role(t, definition), tools = typedTools({ resolved, library: true, tracker: true, arbiter: true });
|
||||||
|
const expected = ['send_message', ...(['companion', 'research'].includes(definition) ? ['web_search', 'web_fetch'] : []), ...LOOKUPS, ...WRITES];
|
||||||
|
assert.deepEqual(tools.map(tool => tool.name), expected, definition);
|
||||||
|
assert.equal(tools.length, ['companion', 'research'].includes(definition) ? 10 : 8);
|
||||||
|
assert.equal(untypedActions(resolved, tools).includes('library.write'), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('C2-02 schemas: both write tools require universe/path/base, close keys, map verb and action', t => {
|
||||||
|
const tools = typedTools({ resolved: role(t, 'ideas'), library: true });
|
||||||
|
for (const [name, verb, required] of [
|
||||||
|
['library_doc_write', 'library.doc.write', ['universe', 'path', 'base', 'content']],
|
||||||
|
['library_doc_delete', 'library.doc.delete', ['universe', 'path', 'base']],
|
||||||
|
]) {
|
||||||
|
const tool = tools.find(tool => tool.name === name);
|
||||||
|
assert.ok(tool, name); assert.equal(tool.verb, verb); assert.equal(tool.action, 'library.write');
|
||||||
|
assert.equal(tool.parameters.type, 'object'); assert.equal(tool.parameters.additionalProperties, false);
|
||||||
|
assert.deepEqual(tool.parameters.required, required);
|
||||||
|
assert.deepEqual(Object.keys(tool.parameters.properties).sort(), [...required].sort());
|
||||||
|
assert.equal(tool.parameters.properties.universe.type, 'string');
|
||||||
|
assert.equal(tool.parameters.properties.path.type, 'string');
|
||||||
|
assert.deepEqual(tool.parameters.properties.base.type, ['string', 'null']);
|
||||||
|
assert.equal(tool.parameters.properties.base.pattern, '^[a-f0-9]{40}$');
|
||||||
|
if (name === 'library_doc_write') assert.equal(tool.parameters.properties.content.type, 'string');
|
||||||
|
for (const field of ['repo', 'commit', 'business', 'accept', 'project', 'identity', 'author', 'credential', 'agent', 'message'])
|
||||||
|
assert.equal(Object.hasOwn(tool.parameters.properties, field), false, `${name} ${field}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('C2-02 tools: no library, noncreative or no within-role write grant exposes neither write tool', t => {
|
||||||
|
const creative = role(t, 'ideas');
|
||||||
|
assert.equal(typedTools({ resolved: creative }).some(tool => WRITES.includes(tool.name)), false);
|
||||||
|
for (const instance of ['coder', 'reviewer', 'pm', 'chief'])
|
||||||
|
assert.equal(typedTools({ resolved: resolvedFor(t, instance), library: true }).some(tool => WRITES.includes(tool.name)), false);
|
||||||
|
const narrowed = structuredClone(creative);
|
||||||
|
narrowed.limits.authority.withinRole = narrowed.limits.authority.withinRole.filter(action => action !== 'library.write');
|
||||||
|
narrowed.limits.authority.crossRole.push('library.write');
|
||||||
|
assert.deepEqual(typedTools({ resolved: narrowed, library: true }).map(tool => tool.name), ['send_message', ...LOOKUPS]);
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@ import { SAFE, git } from '../../project/src/git.mjs';
|
|||||||
import { checkMapping } from './mapping.mjs';
|
import { checkMapping } from './mapping.mjs';
|
||||||
import { LibraryError } from './problems.mjs';
|
import { LibraryError } from './problems.mjs';
|
||||||
import { readLibrary } from './reader.mjs';
|
import { readLibrary } from './reader.mjs';
|
||||||
|
import { indexFor } from './index-build.mjs';
|
||||||
import { SYNC_STATE_FILE, ensureMirror, withLibraryLock, servedCommit } from './mirror.mjs';
|
import { SYNC_STATE_FILE, ensureMirror, withLibraryLock, servedCommit } from './mirror.mjs';
|
||||||
|
|
||||||
export const ASKPASS = fileURLToPath(new URL('../bin/askpass', import.meta.url));
|
export const ASKPASS = fileURLToPath(new URL('../bin/askpass', import.meta.url));
|
||||||
@@ -262,7 +263,7 @@ export async function publishLibraryWrite(mirror, { mapping, commit, previous, r
|
|||||||
const state = await readSyncState({ branch: mapping.branch });
|
const state = await readSyncState({ branch: mapping.branch });
|
||||||
if (state.diverged !== null) fail('library-diverged');
|
if (state.diverged !== null) fail('library-diverged');
|
||||||
if (state.served !== previous) fail('library-state-invalid');
|
if (state.served !== previous) fail('library-state-invalid');
|
||||||
const read = await readLibrary({ repo: mirror, commit, mapping });
|
const read = await indexFor({ repo: mirror, commit, mapping });
|
||||||
const at = now().toISOString();
|
const at = now().toISOString();
|
||||||
const next = { ...state, served: commit, lastPush: { commit, at }, lastError: null,
|
const next = { ...state, served: commit, lastPush: { commit, at }, lastError: null,
|
||||||
problems: read.problems.map(p => ({ ...p })) };
|
problems: read.problems.map(p => ({ ...p })) };
|
||||||
|
|||||||
Reference in new issue
Block a user