Untested: add library write tools and served blob integration (#1793)

This commit is contained in:
jason.woltje committed 2026-10-23 14:45:31 -05:00
1 parent 29455c0c5d
commit cd2f50daec
4 files changed
+91 -2

No files matched your search

+22 -1
View File
@@ -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);
});
+13
View File
@@ -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]);
});
+2 -1
View File
@@ -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 })) };