row 285: fixtures, example and README give the chief and creative instances their gitea reference (#1790, mos-1330)
Fixture data only, plus two named tests that a chief or creative instance without credentials.gitea refuses at load. chief-of-staff.test :88 and :155 change expected values and wait on Mos's mos-1272 ruling. Nothing here has run.
This commit is contained in:
1 parent
ca90d28bd1
commit
362d1ebe3b
11 files changed
+75
-31
No files matched your search
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { Broker } from '../src/broker.mjs';
|
||||
import { startBroker } from '../src/runtime.mjs';
|
||||
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
|
||||
import { businessDoc, FAR_DATE, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
|
||||
|
||||
// Row 213 (C2-08, #1714, Case 2 design 5.1): a creative instance talks with the user only, and it has
|
||||
// no decision channel (M6). The messaging lists are read from the shipped role files.
|
||||
@@ -98,7 +98,7 @@ async function runtime(t) {
|
||||
const rolesDir = rolesCopy(root);
|
||||
const doc = businessDoc(root, 'demo');
|
||||
doc.plugins = { ...doc.plugins, harness: ['pi'] };
|
||||
doc.roles.bean = { definition: 'companion' };
|
||||
doc.roles.bean = { definition: 'companion', credentials: { gitea: { store: true, rotateBy: FAR_DATE } } };
|
||||
for (const r of Object.values(doc.roles)) r.vars = { harness: 'pi', model: 'claude-sonnet-5-5', thinking: 'high', ...r.vars };
|
||||
// Only bean names the sonnet key, so the chief may still change every other key.
|
||||
for (const [name, r] of Object.entries(doc.roles)) if (name !== 'bean') r.vars.model = 'claude-opus-5-5';
|
||||
|
||||
@@ -167,7 +167,7 @@ thread run, and it configures the assistant through `assistants` (Assistant
|
||||
settings, below) and names instances through `set_name`. PRD 1.7 REQ-COS-3
|
||||
(row 194) grants it `role.hire` and `role.fire` within role: it hires and
|
||||
fires with a reason and no decision, through the broker's `instance.hire` and
|
||||
`instance.fire`, which the harness's `hire` and `fire` tools call. It has no credentials and the same read-only tools. It is the only shipped role that holds `role.launch`, so
|
||||
`instance.fire`, which the harness's `hire` and `fire` tools call. It has the same read-only tools, and its one credential reference is gitea, for the read grant the library mirror fetches with (row 285). The broker holds that token, never the agent. It is the only shipped role that holds `role.launch`, so
|
||||
it is the only role `launch.by` can name. The PM holds no `role.launch`
|
||||
(REQ-LAUNCH-1); it asks the chief of staff for a session.
|
||||
|
||||
@@ -178,7 +178,7 @@ use the shared, broker-backed organization writer. The instance names are the ow
|
||||
example file's:
|
||||
|
||||
```json
|
||||
"chief": { "definition": "chief-of-staff", "vars": { "model": "opus" } },
|
||||
"chief": { "definition": "chief-of-staff", "credentials": { "gitea": { "file": "/home/you/.config/mosaic-dev/secrets/mosaic-stack/chief-of-staff-gitea.token", "rotateBy": "YYYY-MM-DD" } }, "vars": { "model": "opus" } },
|
||||
"assistant": { "definition": "assistant", "holder": "mos", "vars": { "model": "opus" } }
|
||||
...
|
||||
"launch": { "by": "chief", "instances": ["coder", "reviewer"], "models": {
|
||||
@@ -187,8 +187,10 @@ example file's:
|
||||
}, "limits": { "models": { "anthropic": { "claude-opus-5-5": 4, "claude-sonnet-5-5": 4 } } } }
|
||||
```
|
||||
|
||||
Neither entry has a `tracker` or `credentials`, because neither role holds
|
||||
any. A session that launches needs a harness and a model, so give each
|
||||
Neither entry has a `tracker`. The chief's `credentials` must name exactly
|
||||
its definition's services, gitea alone, and the assistant has none. A
|
||||
`{ "store": true, "rotateBy": "YYYY-MM-DD" }` reference works as well as a
|
||||
file. Neither form creates an account, mints a token or grants anything. A session that launches needs a harness and a model, so give each
|
||||
instance `vars` (`harness`, `model`) as the PM has, unless the system's
|
||||
execution settings cover it. A `launch.by` that names a PM, an assistant or any other instance
|
||||
whose role lacks `role.launch` is refused. A file that still names the PM
|
||||
|
||||
@@ -113,6 +113,12 @@
|
||||
},
|
||||
"chief": {
|
||||
"definition": "chief-of-staff",
|
||||
"credentials": {
|
||||
"gitea": {
|
||||
"file": "/home/you/.config/mosaic-dev/secrets/mosaic-stack/chief-of-staff-gitea.token",
|
||||
"rotateBy": "YYYY-MM-DD"
|
||||
}
|
||||
},
|
||||
"vars": {
|
||||
"model": "opus"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { BusinessError, businessFilePath, configDir, loadBusiness, validateBusinessDocument } from "../src/index.mjs";
|
||||
import { THREAD_IDLE_SECONDS } from "../src/business.mjs";
|
||||
import { CREATIVE_DEFINITIONS } from "../src/vocabulary.mjs";
|
||||
import { launchDoc, businessDoc, REPO, REPO_ROLES, rolesCopy, tmp, writeJson } from "./helpers.mjs";
|
||||
|
||||
test('C2-04: library mappings load without roots, remain frozen, and retain mapping refusals', async t => {
|
||||
@@ -273,28 +274,56 @@ test("the shipped example refuses as written and validates once filled in", () =
|
||||
assert.equal(id, 15);
|
||||
});
|
||||
|
||||
// Row 285 (C2-08b, #1790, mos-1330): a creative definition declares gitea, so its instance names
|
||||
// the reference. The store form needs no token file, and the bus reads a missing one as no token.
|
||||
const CREATIVE_GITEA = Object.freeze({ gitea: Object.freeze({ store: true, rotateBy: "2099-01-01" }) });
|
||||
|
||||
// Row 213 (C2-08, #1714), deviation D1: the persona vars sit on a creative instance only, and
|
||||
// both or neither. The var checks themselves are vars.mjs's.
|
||||
test("persona vars: agent layer, creative definition only, both or neither", () => {
|
||||
const sha = "a".repeat(64);
|
||||
const persona = { "persona.name": "Bean", "persona.sha256": sha };
|
||||
const business = check((d) => { d.roles.bean = { definition: "companion", vars: { ...persona } }; });
|
||||
const business = check((d) => { d.roles.bean = { definition: "companion", credentials: CREATIVE_GITEA, vars: { ...persona } }; });
|
||||
assert.deepEqual({ ...business.roles.bean.vars }, persona);
|
||||
assert.doesNotThrow(() => check((d) => { d.roles.bean = { definition: "ideas" }; }));
|
||||
assert.doesNotThrow(() => check((d) => { d.roles.bean = { definition: "ideas", credentials: CREATIVE_GITEA }; }));
|
||||
refuses(() => check((d) => { d.vars["persona.name"] = "Bean"; }), /persona\.name can't be set at the business layer/);
|
||||
refuses(() => check((d) => { d.roles.pm.vars = { ...d.roles.pm.vars, ...persona }; }),
|
||||
/roles\.pm\.vars: persona vars need a creative definition \(pm isn't one\)/);
|
||||
refuses(() => check((d) => { d.roles.chief.vars = { ...persona }; }), /chief-of-staff isn't one/);
|
||||
for (const key of Object.keys(persona)) {
|
||||
refuses(() => check((d) => { d.roles.bean = { definition: "research", vars: { [key]: persona[key] } }; }),
|
||||
refuses(() => check((d) => { d.roles.bean = { definition: "research", credentials: CREATIVE_GITEA, vars: { [key]: persona[key] } }; }),
|
||||
/roles\.bean\.vars: persona\.name and persona\.sha256 are set together or not at all/);
|
||||
}
|
||||
for (const [key, value] of [["persona.name", "x".repeat(41)], ["persona.name", "Be\nan"], ["persona.name", "Be\u0007an"], ["persona.name", "Be\u2028an"], ["persona.name", "Be\u2029an"], ["persona.name", 7],
|
||||
["persona.sha256", "A".repeat(64)], ["persona.sha256", sha.slice(1)], ["persona.sha256", `${sha}0`]]) {
|
||||
refuses(() => check((d) => { d.roles.bean = { definition: "companion", vars: { ...persona, [key]: value } }; }), new RegExp(`variable ${key.replace(".", "\\.")}`), 2);
|
||||
refuses(() => check((d) => { d.roles.bean = { definition: "companion", credentials: CREATIVE_GITEA, vars: { ...persona, [key]: value } }; }), new RegExp(`variable ${key.replace(".", "\\.")}`), 2);
|
||||
}
|
||||
});
|
||||
|
||||
// Row 285 (C2-08b, #1790, mos-1330): the chief and each creative definition declare gitea, so an
|
||||
// instance without the reference refuses at load with the credential map's message.
|
||||
test("row 285: a chief or creative instance without credentials.gitea refuses at load", () => {
|
||||
refuses(() => check((d) => { delete d.roles.chief.credentials; }),
|
||||
/roles\.chief\.credentials must reference exactly the services its role definition needs \(gitea; got none\)/);
|
||||
for (const definition of CREATIVE_DEFINITIONS) {
|
||||
refuses(() => check((d) => { d.roles.bean = { definition }; }),
|
||||
/roles\.bean\.credentials must reference exactly the services its role definition needs \(gitea; got none\)/);
|
||||
assert.doesNotThrow(() => check((d) => { d.roles.bean = { definition, credentials: CREATIVE_GITEA }; }), definition);
|
||||
}
|
||||
});
|
||||
|
||||
// Row 285 (C2-08b, brief): a creative instance may carry identity and publish, under the rules at
|
||||
// business.mjs:155-166, which the grant path reads. The grant itself still refuses it (#giteaRole).
|
||||
test("row 285: a creative instance with identity and publish loads", () => {
|
||||
const identity = { name: "Bean", email: "[email protected]", gitea: { url: "https://git.example", user: "acme-bean" } };
|
||||
const business = check((d) => {
|
||||
d.projects.stack.remote = "https://git.example/acme/stack";
|
||||
d.roles.bean = { definition: "companion", credentials: CREATIVE_GITEA, identity, publish: ["stack"] };
|
||||
});
|
||||
assert.deepEqual({ ...business.roles.bean.identity, gitea: { ...business.roles.bean.identity.gitea } }, identity);
|
||||
assert.deepEqual([...business.roles.bean.publish], ["stack"]);
|
||||
});
|
||||
|
||||
// Row 194 (#1692, D2.5, T17): the per-agent forbid and the retired list.
|
||||
test("row 194 keep, noHire and retired validate, and a file without them loads as before", () => {
|
||||
const plain = check();
|
||||
|
||||
@@ -10,7 +10,7 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { ACTIONS, BusinessError, classify, loadBusiness, loadRole, resolveInstance } from "../src/index.mjs";
|
||||
import { THREAD_IDLE_SECONDS } from "../src/business.mjs";
|
||||
import { businessDoc, launchDoc, REPO_ROLES, systemFor, tmp, writeJson } from "./helpers.mjs";
|
||||
import { businessDoc, FAR_DATE, launchDoc, REPO_ROLES, systemFor, tmp, writeJson } from "./helpers.mjs";
|
||||
|
||||
// Decision 104's list plus role.launch (decision 139) plus role.end and
|
||||
// organization.change (row 123, REQ-COS-1) and record.write (row 138), plus instance.name
|
||||
@@ -20,7 +20,8 @@ const APPROVED = Object.freeze({
|
||||
tools: ["read", "grep", "find", "ls"],
|
||||
network: "api-only",
|
||||
authority: Object.freeze({ withinRole: Object.freeze(["message.send", "role.launch", "role.end", "organization.change", "record.write", "instance.name", "role.hire", "role.fire"]), crossRole: Object.freeze([]) }),
|
||||
credentials: Object.freeze([]),
|
||||
// Row 285 (#1790, mos-1309 item 5, sage-2915): gitea for the library read grant, read scopes only.
|
||||
credentials: Object.freeze([Object.freeze({ service: "gitea", scopes: Object.freeze(["read:repository", "read:user"]) })]),
|
||||
// Row 194 (#1692, Q4, sage-1374): empty until a policy commit after the PRD 1.7 resolve.
|
||||
hireable: Object.freeze([]),
|
||||
});
|
||||
@@ -79,13 +80,17 @@ test("the shipped file is exactly the approved list", () => {
|
||||
});
|
||||
|
||||
test("the resolved record matches the approved list, so a widened file fails", () => {
|
||||
const record = resolved(loadFixture());
|
||||
const fixture = loadFixture();
|
||||
const record = resolved(fixture);
|
||||
assert.deepEqual(record.limits, {
|
||||
tools: APPROVED.tools,
|
||||
network: APPROVED.network,
|
||||
authority: APPROVED.authority,
|
||||
});
|
||||
assert.deepEqual(record.credentials, {});
|
||||
// Row 285 (mos-1330): the fixture's gitea reference, the chief's only one.
|
||||
assert.deepEqual(record.credentials, {
|
||||
gitea: { service: "gitea", file: join(fixture.root, "secrets", "chief-of-staff-gitea.token"), rotateBy: FAR_DATE },
|
||||
});
|
||||
});
|
||||
|
||||
test("the chief of staff holds role.launch, message.send, role.end, organization.change, record.write, instance.name, role.hire and role.fire; every other action is gated", () => {
|
||||
@@ -149,12 +154,13 @@ test("the contract names no duty to talk to the human and the pm contract no lau
|
||||
assert.match(pm, /You don't launch sessions/);
|
||||
});
|
||||
|
||||
test("the instance takes no credential references and no tracker", () => {
|
||||
// Row 285 (mos-1330): the instance references exactly gitea, its definition's one service, and no tracker.
|
||||
test("the instance takes only the gitea credential reference and no tracker", () => {
|
||||
refuses(
|
||||
() => loadFixture((doc) => {
|
||||
doc.roles[INSTANCE].credentials = { gitea: { file: "/x/token", rotateBy: "2099-01-01" } };
|
||||
doc.roles[INSTANCE].credentials.vikunja = { file: "/x/token", expires: "2099-01-01" };
|
||||
}),
|
||||
/must reference exactly the services its role definition needs \(none; got gitea\)/,
|
||||
/must reference exactly the services its role definition needs \(gitea; got gitea, vikunja\)/,
|
||||
);
|
||||
refuses(
|
||||
() => loadFixture((doc) => {
|
||||
|
||||
@@ -68,9 +68,10 @@ export function businessDoc(root, id = "acme") {
|
||||
cto: role("cto", 4, { holder: "darkwing", vars: { harness: "claude-code" } }),
|
||||
coder: role("coder", 5, { vars: { harness: "pi", "limits.network": "none" } }),
|
||||
reviewer: role("reviewer", 6),
|
||||
// The chief of staff launches (REQ-LAUNCH-1, decision 139); it holds no
|
||||
// credential and no tracker bot.
|
||||
chief: { definition: "chief-of-staff" },
|
||||
// The chief of staff launches (REQ-LAUNCH-1, decision 139); it has no
|
||||
// tracker bot. Its one credential reference is gitea, for the library
|
||||
// read grant (row 285, mos-1330).
|
||||
chief: { definition: "chief-of-staff", credentials: { gitea: ref("chief-of-staff", "gitea") } },
|
||||
},
|
||||
launch: launchDoc(),
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync }
|
||||
import { join } from "node:path";
|
||||
import { instanceSettings } from "../src/instance-settings.mjs";
|
||||
import { projectFilePath } from "../src/project.mjs";
|
||||
import { businessDoc, REPO, rolesCopy, systemConfig, systemFor, tmp, writeJson } from "./helpers.mjs";
|
||||
import { businessDoc, FAR_DATE, REPO, rolesCopy, systemConfig, systemFor, tmp, writeJson } from "./helpers.mjs";
|
||||
|
||||
const CLI = join(REPO, "packages", "business", "src", "cli.mjs");
|
||||
const roots = [];
|
||||
@@ -331,8 +331,8 @@ test("row 213 a creative instance is the human's: add, vars and definition chang
|
||||
// every actor, the human included. The listing's endProtected is that same check for its reader.
|
||||
test("row 256 T1-T3, T5 the human deletes a creative instance, no other actor does, and the assistant stays", () => {
|
||||
const f = fixture(doc => {
|
||||
doc.roles.bean = { definition: "companion" };
|
||||
doc.roles.muse = { definition: "ideas" };
|
||||
doc.roles.bean = { definition: "companion", credentials: { gitea: { store: true, rotateBy: FAR_DATE } } };
|
||||
doc.roles.muse = { definition: "ideas", credentials: { gitea: { store: true, rotateBy: FAR_DATE } } };
|
||||
doc.roles.assistant = { definition: "assistant", holder: "mos" };
|
||||
});
|
||||
const del = (instance, actor) => () => f.settings.set("acme", { revision: f.settings.read("acme").revision, instance, op: "delete" }, actor);
|
||||
@@ -377,7 +377,7 @@ test("row 256 T1-T3, T5 the human deletes a creative instance, no other actor do
|
||||
// instance can't be kept, launch.by or an arbiter, so the check it reaches is the last-listed one.
|
||||
test("row 256 T4 the human's delete of the only listed instance, a creative one, is the last-listed refusal", () => {
|
||||
const f = fixture(doc => {
|
||||
doc.roles.bean = { definition: "companion" };
|
||||
doc.roles.bean = { definition: "companion", credentials: { gitea: { store: true, rotateBy: FAR_DATE } } };
|
||||
doc.launch.instances = ["bean"];
|
||||
});
|
||||
const bytes = readFileSync(f.file, "utf8");
|
||||
|
||||
@@ -181,7 +181,7 @@ test('B7 kept instance: its route is the human\'s',async()=>{
|
||||
// can't reach its model through the models table, the instances key or a route; the human can,
|
||||
// except through the instances key, which refuses a creative instance for everyone.
|
||||
test('M10 creative instance: models keys it names, the instances key and its route are the human\'s',async()=>{
|
||||
const f=fixture();const doc=structuredClone(f.doc);doc.roles.bean={definition:'companion',vars:{model:'sonnet'}};writeJson(f.file,doc);
|
||||
const f=fixture();const doc=structuredClone(f.doc);doc.roles.bean={definition:'companion',credentials:{gitea:{store:true,rotateBy:FAR_DATE}},vars:{model:'sonnet'}};writeJson(f.file,doc);
|
||||
const g={...f,doc};const {settings}=await routeSettings(g);const file=()=>readFileSync(f.file,'utf8');
|
||||
const chief='chief-of-staff.chief';let {revision}=settings.read('acme');
|
||||
const models=structuredClone(settings.read('acme').models);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { join } from "node:path";
|
||||
import { instanceSettings } from "../src/instance-settings.mjs";
|
||||
import { OrganizationError, organizationWriter } from "../src/organization.mjs";
|
||||
import { CREATIVE_DEFINITIONS } from "../src/vocabulary.mjs";
|
||||
import { businessDoc, REPO, rolesCopy, systemConfig, systemFor, tmp, writeJson } from "./helpers.mjs";
|
||||
import { businessDoc, FAR_DATE, REPO, rolesCopy, systemConfig, systemFor, tmp, writeJson } from "./helpers.mjs";
|
||||
|
||||
const CLI = join(REPO, "packages", "business", "src", "cli.mjs");
|
||||
const roots = [];
|
||||
@@ -235,7 +235,7 @@ test("D2.2 fire delists and keeps the entry, a repeat is done with no write, and
|
||||
// every actor's fire, the human's included.
|
||||
test("row 256 checkDelete passes the human's creative delete and refuses another actor's; fire still refuses", () => {
|
||||
const f = fixture(doc => {
|
||||
doc.roles.bean = { definition: "companion" };
|
||||
doc.roles.bean = { definition: "companion", credentials: { gitea: { store: true, rotateBy: FAR_DATE } } };
|
||||
doc.launch.instances.push("bean");
|
||||
});
|
||||
const bytes = readFileSync(f.file, "utf8");
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
||||
import {
|
||||
BusinessError, classify, projectFilePath, resolveInstance, systemVars, validateBusinessDocument, validateProjectDocument,
|
||||
} from "../src/index.mjs";
|
||||
import { businessDoc, REPO_ROLES, systemFor, tmp } from "./helpers.mjs";
|
||||
import { businessDoc, FAR_DATE, REPO_ROLES, systemFor, tmp } from "./helpers.mjs";
|
||||
|
||||
function refuses(fn, pattern) {
|
||||
assert.throws(fn, (error) => {
|
||||
@@ -114,7 +114,7 @@ test("role.launch stays within-role only for the instance the launch block names
|
||||
|
||||
// A second chief-of-staff instance is the launcher, so the first one may not launch.
|
||||
const two = setup((d) => {
|
||||
d.roles.chief2 = { definition: "chief-of-staff" };
|
||||
d.roles.chief2 = { definition: "chief-of-staff", credentials: { gitea: { store: true, rotateBy: FAR_DATE } } };
|
||||
d.launch.by = "chief2";
|
||||
});
|
||||
assert.equal(classify(resolve(two, "chief"), "role.launch"), "gated");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { launchDoc } from "../../business/tests/helpers.mjs";
|
||||
import { FAR_DATE, launchDoc } from "../../business/tests/helpers.mjs";
|
||||
// The S6 launcher inside a real bus host: real broker child, real runners
|
||||
// in the bwrap sandbox, the fake adapter from packages/harness standing in for the
|
||||
// harness. The chief of staff launches a coder through its `launch` tool (row 119: the
|
||||
@@ -766,7 +766,7 @@ test("row 64 a role skill that isn't enabled refuses skill-not-enabled, and one
|
||||
test("row 213 a creative instance's persona is read from <configDir>/personas: off its digest the launch fails with the mismatch as its reason", { timeout: 30000 }, async (t) => {
|
||||
const text = "Warm. PERSONA-MARKER-d2\n";
|
||||
const given = prepare(t, (doc) => {
|
||||
doc.roles.bean = { definition: "companion", vars: { harness: "pi", model: "claude-sonnet-5-5", thinking: "high", "persona.name": "Bean", "persona.sha256": createHash("sha256").update(text).digest("hex") } };
|
||||
doc.roles.bean = { definition: "companion", credentials: { gitea: { store: true, rotateBy: FAR_DATE } }, vars: { harness: "pi", model: "claude-sonnet-5-5", thinking: "high", "persona.name": "Bean", "persona.sha256": createHash("sha256").update(text).digest("hex") } };
|
||||
doc.launch.instances.push("bean");
|
||||
return doc;
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user