Requirement 001 phase 6, S0: - Speech: the security class shows as "Sécurité" and is spoken as in maritime radio, "say-cure-ee-tay" three times. A hex run of 7 or more characters with a letter and a digit (commit, session, message ids) is spelled in the NATO alphabet; every other digit is spoken on its own in ICAO pronunciation (42305: four two tree zero fife). The spelling runs inside clip() before the length cut, so a phrase still fits SPEECH_LIMIT; the displayed text keeps its digits and hashes. - English: every orchestrator label, tooltip, message, error, script output, document and test string is English (Regeln -> Firewall, Hierarchie -> Hierarchy, Steuerung -> Controls, Ausführen -> Run, ...); the page declares lang="en" and formats dates as en-GB. Task filter ids are English. German stays only in the skill trigger phrases that quote the operator, and as identifiers: the view keys (view=) and the leitstand id. - tests/test_english.py scans every tracked text file for umlauts and German words, with an allowlist of the quoted trigger phrases. BREAKING CHANGE: the Stream search tokens art: and seit: are now kind: and since:; an old art: or seit: token counts as a plain word. Evidence: make test 657 OK, ruff and markdownlint clean (/tmp/s0-make-test-1.log); e2e 17 OK, none skipped (/tmp/s0-e2e-1.log); MEDUSA 86 files, 24 findings on reworded strings accepted after an independent skeptic (21 path:line, 0 rejected, no behaviour change in signing, approving, rules, denying, offices, events), rescan 0 open. 🎯 Fix: 93 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: – 🛡️ Security: 100 % 🏆 Score: 89 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
4.8 KiB
JavaScript
76 lines
4.8 KiB
JavaScript
const RULE_KINDS = {ask: "Ask: tool + command pattern", review: "Review: branch pattern + gate green", decision: "Decision: take the recommendation"};
|
||
const RULE_HINTS = {
|
||
ask: "Grants matching asks in matching directories, never a command with a warning (rm -r, push, --force, sudo, pipes into a shell …). The passkey signs the rule once; the hook checks signature and pattern itself.",
|
||
review: "Grants reviews of matching branches and worktrees, only when their gate is green. The passkey signs the rule once; the commit skill checks signature and pattern itself.",
|
||
decision: "Takes the recommended option in decisions of matching agents. The passkey signs the rule once; the decisions themselves stay unsigned.",
|
||
};
|
||
|
||
function rulesScreen() {
|
||
const kind = h("select", {class: "field", "aria-label": "Kind"}, Object.entries(RULE_KINDS).map(([value, label]) => h("option", {value}, label)));
|
||
const tool = h("input", {class: "field", type: "text", placeholder: "Bash", value: "Bash"});
|
||
const pattern = h("input", {class: "field", type: "text", placeholder: "make test*"});
|
||
const scope = h("input", {class: "field", type: "text", value: "*", placeholder: "/home/…/Repositories/*"});
|
||
const expiry = new Date(Date.now() + 7 * DAY * 1000);
|
||
const expires = h("input", {class: "field", type: "date", value: localInput(expiry.getTime() / 1000).slice(0, 10), required: true});
|
||
const enabled = h("input", {type: "checkbox", checked: true});
|
||
const toolField = h("label", {}, "Tool (glob)", tool);
|
||
const scopeField = h("label", {}, "Directory (glob)", scope);
|
||
const hint = h("p", {class: "muted"});
|
||
const sync = () => {
|
||
toolField.hidden = kind.value !== "ask";
|
||
scopeField.hidden = kind.value === "decision";
|
||
pattern.placeholder = {ask: "make test*", review: "feature/*", decision: "* (agent pattern)"}[kind.value];
|
||
hint.textContent = RULE_HINTS[kind.value];
|
||
};
|
||
kind.addEventListener("change", sync);
|
||
sync();
|
||
const form = h("form", {class: "rule-form"}, h("label", {}, "Kind", kind), toolField, h("label", {}, "Pattern (glob)", pattern), scopeField, h("label", {}, "Expires on", expires), h("label", {class: "check"}, enabled, "active"), h("button", {type: "submit", class: "btn primary"}, "Create rule"));
|
||
form.addEventListener("submit", async (event) => {
|
||
event.preventDefault();
|
||
const end = Date.parse(`${expires.value}T23:59:59`) / 1000;
|
||
const fields = {
|
||
kind: kind.value,
|
||
tool: kind.value === "ask" ? tool.value.trim() : "",
|
||
pattern: pattern.value.trim() || (kind.value === "decision" ? "*" : ""),
|
||
scope: kind.value === "decision" ? "*" : scope.value.trim(),
|
||
expires: Number.isFinite(end) ? end : 0,
|
||
};
|
||
const prepared = await sendJson("/api/rules/sign", fields);
|
||
if (!prepared.ok || !prepared.data) {
|
||
toast("Rule not created", fail(prepared), false);
|
||
return;
|
||
}
|
||
const signed = prepared.data.sign ? await assertion(prepared.data, "Rule") : null;
|
||
if (prepared.data.sign && !signed) {
|
||
return;
|
||
}
|
||
const result = await sendJson("/api/rules", {rule: prepared.data.payload, assertion: signed, enabled: enabled.checked});
|
||
toast(result.ok ? "Rule created" : "Rule not created", result.ok ? [] : fail(result), result.ok);
|
||
await load();
|
||
});
|
||
const change = async (rule, verb) => {
|
||
const result = await send(`/api/rules/${encodeURIComponent(rule.id)}/${verb}`, "", "application/json");
|
||
if (!result.ok) {
|
||
toast("Rule not changed", [`${result.status} ${result.text}`.trim()], false);
|
||
}
|
||
await load();
|
||
};
|
||
const now = nowSeconds();
|
||
const list = state.rules.length ? h("div", {class: "mini"}, state.rules.map((rule) => h("div", {class: "rule-row"},
|
||
pill(rule.kind, KIND_TONES[rule.kind]),
|
||
rule.tool ? h("code", {}, rule.tool) : null,
|
||
h("code", {}, rule.pattern),
|
||
rule.kind === "decision" ? null : h("code", {title: "Directory"}, rule.scope || "–"),
|
||
pill(rule.signed ? "signed" : "unsigned: has no effect", rule.signed ? "ok" : "bad"),
|
||
rule.expires <= now ? pill("expired", "bad") : h("span", {class: "muted"}, `until ${absolute.format(stampOf(rule.expires))}`),
|
||
rule.enabled ? pill("active", "ok") : pill("off", "neutral"),
|
||
h("span", {class: "spacer"}),
|
||
btn(rule.enabled ? "Turn off" : "Turn on", "btn small", () => change(rule, rule.enabled ? "disable" : "enable")),
|
||
btn("Delete", "btn small danger", () => {
|
||
if (window.confirm("Delete rule?")) {
|
||
change(rule, "delete");
|
||
}
|
||
})))) : h("p", {class: "muted"}, "No rule yet. Every automatic grant shows up in the stream and can be undone for 3 s.");
|
||
return h("div", {class: "stack"}, h("h2", {class: "title"}, "Firewall rules for automatic grants"), list, h("div", {class: "card"}, hint, form));
|
||
}
|