toolroll 0.9.89 → 0.9.90
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/CHANGELOG.md +44 -0
- package/README.md +16 -10
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent-board.d.ts +9 -0
- package/dist/agent-board.js +20 -0
- package/dist/agent-fence.js +20 -2
- package/dist/approval-authority.d.ts +7 -0
- package/dist/approval-authority.js +13 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +692 -28
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +13 -0
- package/dist/builder.js +5 -2
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +7 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-task-actions.d.ts +1 -0
- package/dist/check-access.d.ts +3 -0
- package/dist/check-access.js +4 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +6 -2
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +23 -0
- package/dist/coding-ui.js +4 -2
- package/dist/connect-cli.js +2 -1
- package/dist/console-instance.d.ts +34 -1
- package/dist/console-instance.js +124 -15
- package/dist/contracts/cli.d.ts +72 -0
- package/dist/contracts/cli.js +13 -0
- package/dist/contracts/console-api.d.ts +9 -2
- package/dist/contracts/console-api.js +9 -2
- package/dist/desktop-host.js +8 -3
- package/dist/desktop-update.js +2 -1
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +22 -2
- package/dist/easy-start-args.d.ts +13 -0
- package/dist/easy-start-args.js +24 -0
- package/dist/easy-start.d.ts +77 -0
- package/dist/easy-start.js +321 -0
- package/dist/first-run.d.ts +77 -0
- package/dist/first-run.js +45 -0
- package/dist/guides.js +1 -1
- package/dist/invoke.js +11 -6
- package/dist/laptop-background.d.ts +171 -0
- package/dist/laptop-background.js +550 -0
- package/dist/laptop-service.d.ts +111 -0
- package/dist/laptop-service.js +268 -0
- package/dist/lead-cli.js +2 -1
- package/dist/lead-doors.d.ts +6 -0
- package/dist/lead-doors.js +20 -2
- package/dist/live-records.d.ts +1 -1
- package/dist/live-records.js +3 -12
- package/dist/local-owner.d.ts +124 -0
- package/dist/local-owner.js +212 -0
- package/dist/login-secrets.d.ts +60 -0
- package/dist/login-secrets.js +195 -0
- package/dist/mcp-oauth.d.ts +4 -0
- package/dist/mcp-oauth.js +13 -7
- package/dist/model-choices.d.ts +43 -0
- package/dist/model-choices.js +80 -0
- package/dist/operate.d.ts +46 -1
- package/dist/operate.js +461 -40
- package/dist/person-proof.js +3 -0
- package/dist/project-knowledge.d.ts +7 -7
- package/dist/quick-start.d.ts +3 -1
- package/dist/quick-start.js +6 -1
- package/dist/release-cli.js +2 -1
- package/dist/runner-process.d.ts +9 -0
- package/dist/runner-process.js +18 -0
- package/dist/sample-project.d.ts +31 -0
- package/dist/sample-project.js +141 -0
- package/dist/scope.d.ts +24 -2
- package/dist/scope.js +74 -3
- package/dist/serve.js +129 -22
- package/dist/server/chat.js +10 -3
- package/dist/server/chrome.d.ts +10 -2
- package/dist/server/chrome.js +7 -5
- package/dist/server/console.css +10 -0
- package/dist/server/guards.d.ts +3 -0
- package/dist/server/guards.js +22 -2
- package/dist/server/http.d.ts +22 -0
- package/dist/server/http.js +58 -0
- package/dist/server/live.js +4 -14
- package/dist/server/mutation-key.d.ts +70 -0
- package/dist/server/mutation-key.js +142 -0
- package/dist/server/pages.js +10 -7
- package/dist/server/people-tokens.js +240 -20
- package/dist/server/remote.d.ts +2 -0
- package/dist/server/remote.js +19 -4
- package/dist/server/render-new-task.js +5 -1
- package/dist/server/render-pages.d.ts +2 -1
- package/dist/server/render-pages.js +1 -1
- package/dist/server/render-people.d.ts +36 -0
- package/dist/server/render-people.js +65 -1
- package/dist/server/render-projects.js +4 -2
- package/dist/server/render-tasks.d.ts +12 -0
- package/dist/server/render-tasks.js +12 -4
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/route-table.d.ts +16 -0
- package/dist/server/route-table.js +121 -0
- package/dist/server/runtime.d.ts +6 -4
- package/dist/server/session.d.ts +31 -1
- package/dist/server/session.js +12 -2
- package/dist/server/settings.js +63 -8
- package/dist/server/task-views.js +10 -1
- package/dist/server/tasks.d.ts +10 -0
- package/dist/server/tasks.js +59 -9
- package/dist/server/terminals.js +15 -2
- package/dist/server/welcome.d.ts +22 -16
- package/dist/server/welcome.js +150 -52
- package/dist/settings-groups-ui.d.ts +7 -1
- package/dist/settings-groups-ui.js +23 -2
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +124 -1
- package/dist/store.js +245 -12
- package/dist/surface.d.ts +10 -0
- package/dist/surface.js +40 -1
- package/dist/task-text.d.ts +6 -0
- package/dist/task-text.js +17 -0
- package/dist/telegram-lead.js +2 -0
- package/dist/terminal-text.d.ts +11 -0
- package/dist/terminal-text.js +26 -0
- package/dist/toolroll-update.d.ts +8 -1
- package/dist/toolroll-update.js +151 -4
- package/dist/tui-contract.d.ts +118 -0
- package/dist/tui-contract.js +9 -0
- package/dist/tui-source.d.ts +14 -0
- package/dist/tui-source.js +182 -0
- package/dist/tui.js +129 -0
- package/dist/up-processes.d.ts +13 -1
- package/dist/up-processes.js +13 -3
- package/dist/welcome-facts.d.ts +18 -0
- package/dist/welcome-facts.js +79 -0
- package/package.json +4 -2
package/dist/store.js
CHANGED
|
@@ -76,7 +76,8 @@ import { containerEmptiness } from "./container-state.js";
|
|
|
76
76
|
import { hasDisguisedText, hasForbiddenControls, validateNote } from "./decision.js";
|
|
77
77
|
import { parseReviewContext, reviewContextCustodyProblem } from "./review-context.js";
|
|
78
78
|
import { foldReview, manualReviewOnly } from "./proof.js";
|
|
79
|
-
import { liveReleaseOf } from "./scope.js";
|
|
79
|
+
import { hashPassword, liveReleaseOf } from "./scope.js";
|
|
80
|
+
import { currentLocalOwner, LOCAL_MODE_CONFINES, LOCAL_OWNER_LAPSED, localModeOn, localOwnerInFlight, localOwnerStandsNow } from "./local-owner.js";
|
|
80
81
|
import { nightlyDebtHold } from "./nightly-github.js";
|
|
81
82
|
import { releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
82
83
|
import { APPROVAL_BASES, approvalOf, machineActing, personProof, proofDetailOf, quarantinedAuthor, sealOriginOf, digestOf, canonicalProfileJson, chainFromJson, profileDigestOf, profileFromJson, scopeAuthorityOf, routeParityProblem, parseAcceptanceCriteria, exactAcceptance, exactStringList, exactSafeIntegerOrNull } from "./scope.js";
|
|
@@ -1104,7 +1105,12 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
1104
1105
|
// deploy_in_flight, flow_step_task; V145_SCHEMA, new tables only).
|
|
1105
1106
|
// v146 adds, and changes no saved row: each pull request check's own timing as GitHub last reported it for the head the
|
|
1106
1107
|
// plane observed (ci_check), and each release step's start, end and outcome (release_step; V146_SCHEMA, new tables only).
|
|
1107
|
-
|
|
1108
|
+
// v147 adds, and changes no saved row (easy-start plan F): local mode (local_mode, one row, absent = off: only
|
|
1109
|
+
// `up --laptop` on a new database ever writes it), whether an account has a password (approver.password_set; saved rows
|
|
1110
|
+
// read 1), a browser session's authority (web_session.authority; saved rows read 'full'), its local generation, key
|
|
1111
|
+
// epoch and key hash (saved rows read NULL: no local binding, and a stale key), and the running console's key epoch
|
|
1112
|
+
// (console_instance.key_epoch; NULL). V147_ADDED_COLUMNS.
|
|
1113
|
+
export const SCHEMA_VERSION = 147;
|
|
1108
1114
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
1109
1115
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
1110
1116
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -1237,7 +1243,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
1237
1243
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
1238
1244
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
1239
1245
|
*/
|
|
1240
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146]);
|
|
1246
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147]);
|
|
1241
1247
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
1242
1248
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
1243
1249
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1463,6 +1469,35 @@ CREATE TABLE IF NOT EXISTS release_step (
|
|
|
1463
1469
|
);
|
|
1464
1470
|
CREATE INDEX IF NOT EXISTS release_step_by_task ON release_step (task_id, updated_at);
|
|
1465
1471
|
`;
|
|
1472
|
+
/** v147 (easy-start plan F): local mode, one row. Absent is off. Written only by initLocalMode (a database `up --laptop`
|
|
1473
|
+
* created, with no account yet) and changed only through enrollOwnerPassword's precondition and setLocalModeOff, each
|
|
1474
|
+
* of which moves `generation` when it changes what a local-owner session may stand on. `enabled` is the plan's `on`
|
|
1475
|
+
* (ON is an SQL keyword). */
|
|
1476
|
+
export const LOCAL_MODE_SCHEMA = `
|
|
1477
|
+
CREATE TABLE IF NOT EXISTS local_mode (
|
|
1478
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
1479
|
+
owner TEXT NOT NULL REFERENCES approver(name),
|
|
1480
|
+
generation INTEGER NOT NULL CHECK (generation >= 1),
|
|
1481
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),
|
|
1482
|
+
initialized_at TEXT NOT NULL,
|
|
1483
|
+
changed_at TEXT NOT NULL
|
|
1484
|
+
);
|
|
1485
|
+
`;
|
|
1486
|
+
/** v147: an account's password flag, a browser session's authority and local binding, and the console's key epoch.
|
|
1487
|
+
* Every saved row reads the declared value: an account has a password (1), a session is full ('full') with no local
|
|
1488
|
+
* generation, no key epoch (stale: the next mutation asks to reconnect) and no key; the console record has no epoch. */
|
|
1489
|
+
export const V147_ADDED_COLUMNS = Object.freeze({
|
|
1490
|
+
approver: { password_set: { definition: "INTEGER NOT NULL DEFAULT 1 CHECK (password_set IN (0,1))", saved: "1" } },
|
|
1491
|
+
web_session: {
|
|
1492
|
+
authority: { definition: "TEXT NOT NULL DEFAULT 'full' CHECK (authority IN ('full','local-owner'))", saved: "'full'" },
|
|
1493
|
+
local_generation: { definition: "INTEGER CHECK (local_generation IS NULL OR local_generation >= 1)", saved: "NULL" },
|
|
1494
|
+
key_epoch: { definition: "TEXT", saved: "NULL" },
|
|
1495
|
+
key_hash: { definition: "TEXT", saved: "NULL" },
|
|
1496
|
+
},
|
|
1497
|
+
console_instance: { key_epoch: { definition: "TEXT", saved: "NULL" } },
|
|
1498
|
+
});
|
|
1499
|
+
/** The v147 migration's steps after the epoch, in order: a test seam (OpenOptions.migrationStep) may stop after each. */
|
|
1500
|
+
export const V147_STEPS = Object.freeze(["epoch", "local-mode", "password-set", "web-session", "console-instance"]);
|
|
1466
1501
|
/**
|
|
1467
1502
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1468
1503
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -4207,7 +4242,7 @@ export function openStore(file, options = {}) {
|
|
|
4207
4242
|
const db = connect(file);
|
|
4208
4243
|
try {
|
|
4209
4244
|
waitForConcurrentWriter(db);
|
|
4210
|
-
return initializeStore(db, file);
|
|
4245
|
+
return initializeStore(db, file, options);
|
|
4211
4246
|
}
|
|
4212
4247
|
catch (error) {
|
|
4213
4248
|
// An unsuccessful open must not retain an unfinished migration's lock
|
|
@@ -4285,7 +4320,7 @@ CREATE TABLE IF NOT EXISTS team_request (
|
|
|
4285
4320
|
response_json TEXT NOT NULL, PRIMARY KEY(account,generation,request_id)
|
|
4286
4321
|
);
|
|
4287
4322
|
`;
|
|
4288
|
-
function initializeStore(db, file) {
|
|
4323
|
+
function initializeStore(db, file, options = {}) {
|
|
4289
4324
|
// THE PREFLIGHT (v48 integrity): the version is read as EXACTLY ONE
|
|
4290
4325
|
// safe, supported integer before any DDL — one row, a safe integer,
|
|
4291
4326
|
// its magnitude between 1 and this build's version. A database with no
|
|
@@ -4300,7 +4335,7 @@ function initializeStore(db, file) {
|
|
|
4300
4335
|
throw new Error(olderThanBaseline(file, read));
|
|
4301
4336
|
// A fresh file becomes the v107 baseline first, then takes the same steps an installation from 0.5.0 takes.
|
|
4302
4337
|
const fresh = read === null;
|
|
4303
|
-
const preflight = fresh ? createBaseline(db, file) : read;
|
|
4338
|
+
const preflight = fresh ? createBaseline(db, file, options.createdBy) : read;
|
|
4304
4339
|
// History every database since v107 keeps: an open that finds it missing refuses rather than recreate it empty.
|
|
4305
4340
|
if (!tableExists(db, "service_cursor")) {
|
|
4306
4341
|
throw new Error(`${file}: service progress is missing; refusing to silently reset worker cursors`);
|
|
@@ -4404,6 +4439,8 @@ function initializeStore(db, file) {
|
|
|
4404
4439
|
throw new Error(`${file}: refusing to open — the schema version moved from v${preflight} between the preflight and the epoch stamp (another process is migrating it); this build speaks schema v${SCHEMA_VERSION} and alters nothing it cannot name`);
|
|
4405
4440
|
}
|
|
4406
4441
|
}
|
|
4442
|
+
const step = options.migrationStep ?? (() => { });
|
|
4443
|
+
step("epoch");
|
|
4407
4444
|
// v117 renames first, before any step below names a table: they all speak today's names (the lead's, the subagents').
|
|
4408
4445
|
// A fresh file is the v107 baseline by now, under the old names, so it takes the same renames.
|
|
4409
4446
|
renameForV117(db);
|
|
@@ -4726,6 +4763,19 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
4726
4763
|
db.exec(V145_SCHEMA);
|
|
4727
4764
|
// v146: pull request checks' own timing and release steps (new tables only).
|
|
4728
4765
|
db.exec(V146_SCHEMA);
|
|
4766
|
+
// v147: local mode (a new table) and the password flag, session authority and key columns (V147_ADDED_COLUMNS: saved
|
|
4767
|
+
// rows read their declared values). Each step is idempotent: a crash between them resumes here on the next open.
|
|
4768
|
+
db.exec(LOCAL_MODE_SCHEMA);
|
|
4769
|
+
step("local-mode");
|
|
4770
|
+
for (const [column, { definition }] of Object.entries(V147_ADDED_COLUMNS["approver"]))
|
|
4771
|
+
addColumn(db, "approver", column, definition);
|
|
4772
|
+
step("password-set");
|
|
4773
|
+
for (const [column, { definition }] of Object.entries(V147_ADDED_COLUMNS["web_session"]))
|
|
4774
|
+
addColumn(db, "web_session", column, definition);
|
|
4775
|
+
step("web-session");
|
|
4776
|
+
for (const [column, { definition }] of Object.entries(V147_ADDED_COLUMNS["console_instance"]))
|
|
4777
|
+
addColumn(db, "console_instance", column, definition);
|
|
4778
|
+
step("console-instance");
|
|
4729
4779
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
4730
4780
|
// nothing to reclaim.
|
|
4731
4781
|
if (!fresh)
|
|
@@ -4755,7 +4805,7 @@ function olderThanBaseline(file, version) {
|
|
|
4755
4805
|
* file is looked at again under the write lock: a second opener that waited
|
|
4756
4806
|
* on the first finds its tables there and refuses rather than recreating them.
|
|
4757
4807
|
*/
|
|
4758
|
-
function createBaseline(db, file) {
|
|
4808
|
+
function createBaseline(db, file, createdBy) {
|
|
4759
4809
|
db.exec("BEGIN IMMEDIATE");
|
|
4760
4810
|
try {
|
|
4761
4811
|
if (Number(db.prepare("SELECT COUNT(*) AS n FROM sqlite_master").get()?.["n"]) !== 0) {
|
|
@@ -4765,6 +4815,9 @@ function createBaseline(db, file) {
|
|
|
4765
4815
|
db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(BASELINE_SCHEMA_VERSION);
|
|
4766
4816
|
const installedAt = new Date().toISOString();
|
|
4767
4817
|
db.prepare("INSERT INTO installation_fact (key, value, created_at) VALUES ('installed-at', ?, ?)").run(installedAt, installedAt);
|
|
4818
|
+
// v147: who made this database, in the same transaction as installed-at (initLocalMode starts only from this fact).
|
|
4819
|
+
if (createdBy === "laptop")
|
|
4820
|
+
db.prepare("INSERT INTO installation_fact (key, value, created_at) VALUES ('created-by', 'laptop', ?)").run(installedAt);
|
|
4768
4821
|
db.exec("COMMIT");
|
|
4769
4822
|
}
|
|
4770
4823
|
catch (error) {
|
|
@@ -5172,7 +5225,10 @@ function tokenProjectsOf(value) {
|
|
|
5172
5225
|
function readWebSession(row) {
|
|
5173
5226
|
return { idHash: String(row["id_hash"]), account: String(row["account"]), csrf: String(row["csrf"]), role: row["role"] === "viewer" ? "viewer" : "approver", generation: Number(row["generation"]),
|
|
5174
5227
|
createdAt: Number(row["created_at"]), lastSeen: Number(row["last_seen"]), project: row["project"] == null ? null : String(row["project"]), projectRevision: Number(row["project_revision"]),
|
|
5175
|
-
ssoAt: row["sso_at"] == null ? null : Number(row["sso_at"]), agent: row["agent"] == null ? null : String(row["agent"]), address: row["address"] == null ? null : String(row["address"])
|
|
5228
|
+
ssoAt: row["sso_at"] == null ? null : Number(row["sso_at"]), agent: row["agent"] == null ? null : String(row["agent"]), address: row["address"] == null ? null : String(row["address"]),
|
|
5229
|
+
// A value this build can't name is never full authority: it reads local-owner, whose binding (NULL here) stands nowhere.
|
|
5230
|
+
authority: row["authority"] === "full" ? "full" : "local-owner", localGeneration: row["local_generation"] == null ? null : Number(row["local_generation"]),
|
|
5231
|
+
keyEpoch: row["key_epoch"] == null ? null : String(row["key_epoch"]), keyHash: row["key_hash"] == null ? null : String(row["key_hash"]) };
|
|
5176
5232
|
}
|
|
5177
5233
|
/**
|
|
5178
5234
|
* v110: the ledger's source admits 'api' and 'mcp', from the v99 shape; rows, ids and the counter are carried whole, so the v103
|
|
@@ -6014,7 +6070,7 @@ export class Store {
|
|
|
6014
6070
|
// v102: who filed it, set once with the filing (separation of duties reads it; nothing changes it).
|
|
6015
6071
|
if (spec.filedBy !== undefined) {
|
|
6016
6072
|
this.db.prepare("UPDATE task_ref SET filed_by = ?, filed_by_kind = ? WHERE id = ?").run(spec.filedBy.name, spec.filedBy.kind, ref.id);
|
|
6017
|
-
this.recordAction({ at: stamp, actor: spec.filedBy.name ?? "system", repo: null, taskId: spec.id, runId: null, action: "task filed", outcome: "recorded", source: "work", detail: spec.filedBy.kind === "person" ?
|
|
6073
|
+
this.recordAction({ at: stamp, actor: spec.filedBy.name ?? "system", repo: null, taskId: spec.id, runId: null, action: "task filed", outcome: "recorded", source: "work", detail: spec.filedBy.kind === "person" ? localFilingProvenance(spec.filedBy.name) : spec.filedBy.kind });
|
|
6018
6074
|
}
|
|
6019
6075
|
// The deliverable (v34) lands in the SAME transaction as the filing,
|
|
6020
6076
|
// and there is no API to change it afterwards.
|
|
@@ -6566,6 +6622,11 @@ export class Store {
|
|
|
6566
6622
|
this.transacting = true;
|
|
6567
6623
|
let result;
|
|
6568
6624
|
try {
|
|
6625
|
+
// Easy-start C1, the write-time re-check: a local owner's chain writes only while it still stands (local mode on
|
|
6626
|
+
// at its generation, the owner active, its session saved), read inside this same transaction.
|
|
6627
|
+
const local = currentLocalOwner();
|
|
6628
|
+
if (local !== null && localOwnerInFlight(local) && !localOwnerStandsNow(this, local))
|
|
6629
|
+
throw new Error(LOCAL_OWNER_LAPSED);
|
|
6569
6630
|
result = body();
|
|
6570
6631
|
this.db.exec("COMMIT");
|
|
6571
6632
|
}
|
|
@@ -7684,13 +7745,16 @@ export class Store {
|
|
|
7684
7745
|
* generation, revocation — one read, no inference. */
|
|
7685
7746
|
accountOf(name) {
|
|
7686
7747
|
const row = this.db
|
|
7687
|
-
.prepare("SELECT credential_hash, role, generation, revoked_at, projects_json FROM approver WHERE name = ?")
|
|
7748
|
+
.prepare("SELECT credential_hash, password_set, role, generation, revoked_at, projects_json FROM approver WHERE name = ?")
|
|
7688
7749
|
.get(name);
|
|
7689
7750
|
if (row === undefined)
|
|
7690
7751
|
return null;
|
|
7691
7752
|
return {
|
|
7692
7753
|
projects: readProjectAccess(row["projects_json"]),
|
|
7693
7754
|
credentialHash: String(row["credential_hash"]),
|
|
7755
|
+
// v147: a local-mode owner has no password yet (0); its stored hash is random and must never verify. Anything
|
|
7756
|
+
// but exactly 1 reads as no password (fail closed).
|
|
7757
|
+
passwordSet: Number(row["password_set"]) === 1,
|
|
7694
7758
|
role: String(row["role"]) === "viewer" ? "viewer" : "approver",
|
|
7695
7759
|
generation: Number(row["generation"]),
|
|
7696
7760
|
revokedAt: row["revoked_at"] === null || row["revoked_at"] === undefined ? null : String(row["revoked_at"]),
|
|
@@ -8824,9 +8888,11 @@ export class Store {
|
|
|
8824
8888
|
});
|
|
8825
8889
|
}
|
|
8826
8890
|
saveWebSession(session) {
|
|
8827
|
-
|
|
8891
|
+
// v147: authority, local generation, key epoch and key hash are set when the session is made and never changed by a
|
|
8892
|
+
// later save of the same id (ON CONFLICT leaves them): a cached copy can't widen or rebind a kept session.
|
|
8893
|
+
this.db.prepare(`INSERT INTO web_session (id_hash, account, csrf, role, generation, created_at, last_seen, project, project_revision, sso_at, agent, address, authority, local_generation, key_epoch, key_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8828
8894
|
ON CONFLICT(id_hash) DO UPDATE SET last_seen = excluded.last_seen, project = excluded.project, project_revision = excluded.project_revision, sso_at = excluded.sso_at`)
|
|
8829
|
-
.run(session.idHash, session.account, session.csrf, session.role, session.generation, session.createdAt, session.lastSeen, session.project, session.projectRevision, session.ssoAt, session.agent, session.address);
|
|
8895
|
+
.run(session.idHash, session.account, session.csrf, session.role, session.generation, session.createdAt, session.lastSeen, session.project, session.projectRevision, session.ssoAt, session.agent, session.address, session.authority, session.localGeneration, session.keyEpoch, session.keyHash);
|
|
8830
8896
|
}
|
|
8831
8897
|
webSession(idHash) {
|
|
8832
8898
|
const row = this.db.prepare("SELECT * FROM web_session WHERE id_hash = ?").get(idHash);
|
|
@@ -19078,10 +19144,16 @@ export class Store {
|
|
|
19078
19144
|
}
|
|
19079
19145
|
/** Whether this installation lets an agent run where no OS confinement exists (off unless an operator said so). */
|
|
19080
19146
|
unfencedAgentsAllowed() {
|
|
19147
|
+
// Easy-start C5: while local mode is on, this installation always confines, whatever is stored (one chokepoint for
|
|
19148
|
+
// every unconfined fallback: agents, the planner, setup and build commands, checks, flow code, project tools).
|
|
19149
|
+
if (localModeOn(this))
|
|
19150
|
+
return false;
|
|
19081
19151
|
return Number(this.db.prepare("SELECT unfenced_allowed FROM confinement_setting WHERE id = 1").get()?.["unfenced_allowed"] ?? 0) === 1;
|
|
19082
19152
|
}
|
|
19083
19153
|
setUnfencedAgentsAllowed(allowed, by, now) {
|
|
19084
19154
|
this.transact(() => {
|
|
19155
|
+
if (allowed && localModeOn(this))
|
|
19156
|
+
throw new Error(LOCAL_MODE_CONFINES);
|
|
19085
19157
|
const before = this.unfencedAgentsAllowed();
|
|
19086
19158
|
this.db.prepare("UPDATE confinement_setting SET unfenced_allowed = ?, updated_at = ?, updated_by = ? WHERE id = 1").run(allowed ? 1 : 0, now.toISOString(), by);
|
|
19087
19159
|
this.recordPolicy(by, null, "unconfined agents", before ? "allowed" : "refused", allowed ? "allowed" : "refused", now);
|
|
@@ -19999,6 +20071,9 @@ export class Store {
|
|
|
19999
20071
|
return null;
|
|
20000
20072
|
if (widerThan(level, before) && (authority.may !== "widen" || !this.isInstanceOperator(by)))
|
|
20001
20073
|
return null;
|
|
20074
|
+
// Easy-start C5: while local mode is on, no project's checks run unsandboxed (callers say "Turn off local mode first").
|
|
20075
|
+
if (level === "unsandboxed" && localModeOn(this))
|
|
20076
|
+
return null;
|
|
20002
20077
|
this.db.prepare(`INSERT INTO project_check_access (repo, level, updated_at, updated_by) VALUES (?, ?, ?, ?)
|
|
20003
20078
|
ON CONFLICT (repo) DO UPDATE SET level = excluded.level, updated_at = excluded.updated_at, updated_by = excluded.updated_by`).run(repo, level, now.toISOString(), by);
|
|
20004
20079
|
const after = this.projectCheckAccess(repo);
|
|
@@ -22143,3 +22218,161 @@ export function elapsedSeconds(startedAt, endedAt, now) {
|
|
|
22143
22218
|
const start = Date.parse(startedAt), end = endedAt === null ? now.getTime() : Date.parse(endedAt);
|
|
22144
22219
|
return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, Math.round((end - start) / 1000)) : null;
|
|
22145
22220
|
}
|
|
22221
|
+
// ---- v147: local mode (easy-start plan F and C4) ------------------------------------------------------------------
|
|
22222
|
+
/** Local mode as saved, or null when this installation never had it (absent is off). */
|
|
22223
|
+
export function localModeOf(store) {
|
|
22224
|
+
const row = store.handle.prepare("SELECT owner, generation, enabled, initialized_at, changed_at FROM local_mode WHERE id = 1").get();
|
|
22225
|
+
if (row === undefined)
|
|
22226
|
+
return null;
|
|
22227
|
+
const generation = Number(row["generation"]);
|
|
22228
|
+
return { owner: String(row["owner"]), generation: Number.isSafeInteger(generation) ? generation : -1, enabled: Number(row["enabled"]) === 1,
|
|
22229
|
+
initializedAt: String(row["initialized_at"]), changedAt: String(row["changed_at"]) };
|
|
22230
|
+
}
|
|
22231
|
+
/** Who made this database (installation_fact created-by), or null: every database made before v147, or by `serve`. */
|
|
22232
|
+
export function installationCreatedBy(store) {
|
|
22233
|
+
const row = store.handle.prepare("SELECT value FROM installation_fact WHERE key = 'created-by'").get();
|
|
22234
|
+
return row === undefined ? null : String(row["value"]);
|
|
22235
|
+
}
|
|
22236
|
+
/**
|
|
22237
|
+
* Turn local mode on for a database `up --laptop` made, once (plan F). Only when: that database records created-by
|
|
22238
|
+
* laptop, it has no account yet, it never had local mode, and the caller found a fence on this machine (diagnostic
|
|
22239
|
+
* only: it decides whether local mode is offered). One transaction: the owner (no password: a hash of random bytes
|
|
22240
|
+
* that are thrown away, password_set 0), the local_mode row and one ledger line, each inserted only if nothing is
|
|
22241
|
+
* there. A retry or a racing start inserts nothing and writes no second ledger line. Migration and the update
|
|
22242
|
+
* rehearsal never call this.
|
|
22243
|
+
*/
|
|
22244
|
+
export function initLocalMode(store, input) {
|
|
22245
|
+
if (!input.fenceAvailable)
|
|
22246
|
+
return { ok: false, reason: "no-fence" };
|
|
22247
|
+
// Hashed before the transaction (scrypt is slow); the random bytes are never kept anywhere.
|
|
22248
|
+
const unusable = hashPassword(randomBytes(32).toString("hex"));
|
|
22249
|
+
return store.transact(() => {
|
|
22250
|
+
if (installationCreatedBy(store) !== "laptop")
|
|
22251
|
+
return { ok: false, reason: "not-laptop" };
|
|
22252
|
+
if (store.handle.prepare("SELECT 1 FROM local_mode WHERE id = 1").get() !== undefined)
|
|
22253
|
+
return { ok: false, reason: "already" };
|
|
22254
|
+
const stamp = input.now.toISOString();
|
|
22255
|
+
const made = store.handle.prepare(`INSERT INTO approver (name, credential_hash, added_at, generation, password_set)
|
|
22256
|
+
SELECT ?, ?, ?, 1, 0 WHERE NOT EXISTS (SELECT 1 FROM approver) AND NOT EXISTS (SELECT 1 FROM local_mode)`).run(input.owner, unusable, stamp);
|
|
22257
|
+
if (Number(made.changes) !== 1)
|
|
22258
|
+
return { ok: false, reason: "has-accounts" };
|
|
22259
|
+
store.handle.prepare("INSERT INTO local_mode (id, owner, generation, enabled, initialized_at, changed_at) VALUES (1, ?, 1, 1, ?, ?)").run(input.owner, stamp, stamp);
|
|
22260
|
+
store.recordAction({ at: stamp, actor: input.owner, repo: null, taskId: null, runId: null, action: "local mode on", outcome: "initialized", source: "access", detail: "a new laptop install: the owner has no password yet" });
|
|
22261
|
+
return { ok: true, owner: input.owner, generation: 1 };
|
|
22262
|
+
});
|
|
22263
|
+
}
|
|
22264
|
+
/**
|
|
22265
|
+
* The local owner sets their first password (plan C4), in one IMMEDIATE transaction: the account still has none,
|
|
22266
|
+
* local mode is on at the generation the caller's session was bound to, and the account is the active owner. Then the
|
|
22267
|
+
* hash is written with password_set 1 and the account's generation moves, every derived authority from before ends
|
|
22268
|
+
* (credential-rotation: every browser session of the account, local-owner ones included), and the ledger says so.
|
|
22269
|
+
* Local mode stays on. A second, concurrent enrollment finds password_set 1 and gets "already-set" ("A password was
|
|
22270
|
+
* just set — sign in"). `credentialHash` is hashPassword's output; the password itself never reaches the store.
|
|
22271
|
+
*/
|
|
22272
|
+
export function enrollOwnerPassword(store, owner, credentialHash, expectedLocalGeneration, now) {
|
|
22273
|
+
return store.transact(() => {
|
|
22274
|
+
const account = store.accountOf(owner);
|
|
22275
|
+
if (account === null || account.revokedAt !== null || account.role !== "approver")
|
|
22276
|
+
return { ok: false, reason: "not-owner" };
|
|
22277
|
+
if (account.passwordSet)
|
|
22278
|
+
return { ok: false, reason: "already-set" };
|
|
22279
|
+
const mode = localModeOf(store);
|
|
22280
|
+
if (mode === null || mode.owner !== owner)
|
|
22281
|
+
return { ok: false, reason: "not-owner" };
|
|
22282
|
+
if (!mode.enabled || mode.generation !== expectedLocalGeneration)
|
|
22283
|
+
return { ok: false, reason: "local-mode-changed" };
|
|
22284
|
+
const written = store.handle.prepare(`UPDATE approver SET credential_hash = ?, password_set = 1, generation = generation + 1
|
|
22285
|
+
WHERE name = ? AND password_set = 0 AND revoked_at IS NULL AND role = 'approver'`).run(credentialHash, owner);
|
|
22286
|
+
if (Number(written.changes) !== 1)
|
|
22287
|
+
return { ok: false, reason: "already-set" };
|
|
22288
|
+
store.revokeDerivedAuthority(owner, "credential-rotation", now);
|
|
22289
|
+
store.recordAction({ at: now.toISOString(), actor: owner, repo: null, taskId: null, runId: null, action: "password set", outcome: "enrolled", source: "access", detail: `local mode stays on (generation ${mode.generation})` });
|
|
22290
|
+
return { ok: true, generation: account.generation + 1 };
|
|
22291
|
+
});
|
|
22292
|
+
}
|
|
22293
|
+
/**
|
|
22294
|
+
* Turn local mode off (plan C5: one click in Settings for a full session). The caller passes the authority it proved
|
|
22295
|
+
* for this request; only full authority of an active instance operator turns it off. In one transaction: the row reads
|
|
22296
|
+
* off and its generation moves (every local-owner session, link and approval bound to the old one stops standing),
|
|
22297
|
+
* the account's local-owner sessions are deleted, and the ledger says who. Turning it back on is not offered.
|
|
22298
|
+
*/
|
|
22299
|
+
export function setLocalModeOff(store, by, now) {
|
|
22300
|
+
if (by.authority !== "full")
|
|
22301
|
+
return { ok: false, reason: "not-allowed" };
|
|
22302
|
+
return store.transact(() => {
|
|
22303
|
+
if (!store.isInstanceOperator(by.name))
|
|
22304
|
+
return { ok: false, reason: "not-allowed" };
|
|
22305
|
+
const stamp = now.toISOString();
|
|
22306
|
+
const moved = store.handle.prepare("UPDATE local_mode SET enabled = 0, generation = generation + 1, changed_at = ? WHERE id = 1 AND enabled = 1").run(stamp);
|
|
22307
|
+
if (Number(moved.changes) !== 1)
|
|
22308
|
+
return { ok: false, reason: "not-on" };
|
|
22309
|
+
store.handle.prepare("DELETE FROM web_session WHERE authority <> 'full'").run();
|
|
22310
|
+
const mode = localModeOf(store);
|
|
22311
|
+
store.recordAction({ at: stamp, actor: by.name, repo: null, taskId: null, runId: null, action: "local mode off", outcome: "changed", source: "access", detail: `generation ${mode.generation}` });
|
|
22312
|
+
return { ok: true, generation: mode.generation };
|
|
22313
|
+
});
|
|
22314
|
+
}
|
|
22315
|
+
/**
|
|
22316
|
+
* What a local sign-in link is bound to, read now (plan C4): local mode on, and its owner an active approver. Null
|
|
22317
|
+
* otherwise. The console's mintLocalLink records this; redemption reads it again and compares every field.
|
|
22318
|
+
*/
|
|
22319
|
+
export function localOwnerBinding(store) {
|
|
22320
|
+
const mode = localModeOf(store);
|
|
22321
|
+
if (mode === null || !mode.enabled || mode.generation < 1)
|
|
22322
|
+
return null;
|
|
22323
|
+
const account = store.accountOf(mode.owner);
|
|
22324
|
+
if (account === null || account.revokedAt !== null || account.role !== "approver")
|
|
22325
|
+
return null;
|
|
22326
|
+
return { account: mode.owner, localGeneration: mode.generation, accountGeneration: account.generation };
|
|
22327
|
+
}
|
|
22328
|
+
/** easy-start D: "Keep Toolroll running in the background?" is asked once per installation; this fact records it. */
|
|
22329
|
+
export const BACKGROUND_ASKED_FACT = "background-asked";
|
|
22330
|
+
/** The person's one answer to the background question and when it was given, or null when it was never asked. */
|
|
22331
|
+
export function backgroundAsked(store) {
|
|
22332
|
+
const row = store.handle.prepare("SELECT value, created_at FROM installation_fact WHERE key = ?").get(BACKGROUND_ASKED_FACT);
|
|
22333
|
+
if (row === undefined)
|
|
22334
|
+
return null;
|
|
22335
|
+
const value = String(row["value"]);
|
|
22336
|
+
// A value this build doesn't know still means "asked": never ask again on the strength of a word it can't read.
|
|
22337
|
+
return { answer: value === "yes" ? "yes" : "no", at: String(row["created_at"]) };
|
|
22338
|
+
}
|
|
22339
|
+
/** Record the answer, once: the first write wins (installation facts are never updated). True when this call recorded it. */
|
|
22340
|
+
export function recordBackgroundAsked(store, answer, now) {
|
|
22341
|
+
return store.transact(() => {
|
|
22342
|
+
if (backgroundAsked(store) !== null)
|
|
22343
|
+
return false;
|
|
22344
|
+
store.recordInstallationFact(BACKGROUND_ASKED_FACT, answer, now);
|
|
22345
|
+
return true;
|
|
22346
|
+
});
|
|
22347
|
+
}
|
|
22348
|
+
/**
|
|
22349
|
+
* Easy-start C5 provenance: a task a laptop's local owner files (no password, inside its admitted act) says so in its
|
|
22350
|
+
* filing ledger line — `filed via local-owner · local <generation> <browser|terminal>` — so the record shows which local
|
|
22351
|
+
* mode generation and which road it came by. Null for every other filing (a person with a password, the lead, a
|
|
22352
|
+
* machine), which keeps the line as it was. task_ref.filed_via is left to the road it came by (console, mate, cli…).
|
|
22353
|
+
*/
|
|
22354
|
+
export function localFilingProvenance(filer) {
|
|
22355
|
+
const local = currentLocalOwner();
|
|
22356
|
+
if (local === null || !localOwnerInFlight(local) || local.account !== filer)
|
|
22357
|
+
return null;
|
|
22358
|
+
return `filed via local-owner · local ${local.localGeneration} ${local.transport}`;
|
|
22359
|
+
}
|
|
22360
|
+
/**
|
|
22361
|
+
* Easy start: the port a NEW laptop install chose when `up`'s default (4180) was already held by something that isn't
|
|
22362
|
+
* this database's console (another Toolroll, the desktop app, any program). Saved once, the first time `up --laptop`
|
|
22363
|
+
* reserves a port on a database `up --laptop` made, so later starts, the background service's unit and `toolroll open`
|
|
22364
|
+
* all find the same address. Installs made any other way never record one: their port stays `up`'s default.
|
|
22365
|
+
*/
|
|
22366
|
+
export const LAPTOP_PORT_FACT = "laptop-port";
|
|
22367
|
+
/** The saved laptop port, or null (none saved, or a value that isn't a port). */
|
|
22368
|
+
export function laptopPortOf(store) {
|
|
22369
|
+
const value = store.installationFact(LAPTOP_PORT_FACT);
|
|
22370
|
+
if (value === null || !/^\d{1,5}$/.test(value))
|
|
22371
|
+
return null;
|
|
22372
|
+
const port = Number(value);
|
|
22373
|
+
return port >= 1 && port < 65536 ? port : null;
|
|
22374
|
+
}
|
|
22375
|
+
/** Record the laptop port, once (installation facts are never updated). */
|
|
22376
|
+
export function recordLaptopPort(store, port, now) {
|
|
22377
|
+
store.recordInstallationFact(LAPTOP_PORT_FACT, String(port), now);
|
|
22378
|
+
}
|
package/dist/surface.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* SURFACE_NOTES.flags says "intended".
|
|
17
17
|
*/
|
|
18
18
|
import { type CommandFlag, type CommandRow, type RemotePolicy } from './contracts/cli.js';
|
|
19
|
+
import type { LocalOwnerAct } from './local-owner.js';
|
|
19
20
|
/** A guide row and its flags are derived from their schemas in src/contracts/cli.ts. */
|
|
20
21
|
export type { CommandFlag, CommandRow, RemotePolicy };
|
|
21
22
|
/** The guide's limits, stated machine-readably in every dump. */
|
|
@@ -30,6 +31,15 @@ export declare const SURFACE_SCHEMA_VERSION = 1;
|
|
|
30
31
|
export declare const REMOTE_POLICY: ReadonlyMap<string, RemotePolicy>;
|
|
31
32
|
/** How many times each invocation was classified — the contract test refuses a row stated twice. */
|
|
32
33
|
export declare const REMOTE_POLICY_COUNT: number;
|
|
34
|
+
/** Every guide row's local-owner mark, by invocation: `allow` with its act, else `deny`. Exported for the inventory test. */
|
|
35
|
+
export declare const LOCAL_POLICY: ReadonlyMap<string, {
|
|
36
|
+
local: "allow";
|
|
37
|
+
act: LocalOwnerAct;
|
|
38
|
+
} | {
|
|
39
|
+
local: "deny";
|
|
40
|
+
}>;
|
|
41
|
+
/** The invocations LOCAL_ALLOW names, for the inventory test (one naming no row is stale). */
|
|
42
|
+
export declare const LOCAL_ALLOWED_INVOCATIONS: readonly string[];
|
|
33
43
|
/** Every declared command, in guide order, paired with the schema of its `--json` answer (src/contracts/cli.ts).
|
|
34
44
|
* An unclassified row is "no": nothing runs remotely by omission. */
|
|
35
45
|
export declare const COMMAND_ENTRIES: readonly import("./contracts/cli.js").CommandEntry[];
|
package/dist/surface.js
CHANGED
|
@@ -431,6 +431,13 @@ const GUIDE_ROWS = [
|
|
|
431
431
|
operator("up", "console + worker + browser, one command"),
|
|
432
432
|
operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), offer the starter flows (--starter <ids>), and print the handoff"),
|
|
433
433
|
operator("daemon", "install the loop under the OS service manager"),
|
|
434
|
+
operator("background on", "keep this computer's Toolroll running: a login item that starts `up --laptop` when you log in; hands over from the `up` this terminal runs and puts everything back if the service doesn't answer (--port)"),
|
|
435
|
+
operator("background off", "stop running in the background: unloads and removes only Toolroll's own login item"),
|
|
436
|
+
operatorRead("background show", "whether Toolroll runs in the background here, whether it is running now, and any other service on this database"),
|
|
437
|
+
operator("stop", "stop this computer's Toolroll after its running work is saved: the background service (until the next login), or the `up` running in a terminal"),
|
|
438
|
+
operator("open", "open this computer's Toolroll at a terminal, signed in: the terminal UI, or the browser with --web (starts it first when it isn't running); never prints a sign-in link"),
|
|
439
|
+
{ ...operatorRead("welcome", "the three steps to a first result (agent, project, first task) as the console's welcome shows them; at a terminal it walks them one at a time"), flags: [jsonFlag, dbFlag] },
|
|
440
|
+
operator("password set", "set this computer's first password, typed twice at your own terminal (never a flag, file or environment variable); every session signs in again and local mode stays on"),
|
|
434
441
|
{ ...operatorRead("doctor", "check that Toolroll's service holds no key, token or password a build could read; names what it finds and the steps to fix it, never a value, and changes nothing"), flags: [jsonFlag] },
|
|
435
442
|
];
|
|
436
443
|
/**
|
|
@@ -450,7 +457,7 @@ const REMOTE_NO = [
|
|
|
450
457
|
// Workers, the loop and the services that run here.
|
|
451
458
|
"claim", "heartbeat", "worker release", "reap", "tick", "build", "reconcile",
|
|
452
459
|
"runner register", "runner retire", "runner bind", "runner capacity", "coordinator mint", "coordinator revoke", "mcp", "outbox deliver", "peek",
|
|
453
|
-
"serve", "serve check-public", "watch", "up", "onboard", "daemon", "doctor", "bridge", "setup show", "setup clear",
|
|
460
|
+
"serve", "serve check-public", "watch", "up", "onboard", "daemon", "background on", "background off", "background show", "stop", "open", "welcome", "password set", "doctor", "bridge", "setup show", "setup clear",
|
|
454
461
|
// Provider keys and model tools on this machine.
|
|
455
462
|
"providers", "keys status", "keys set", "keys clear", "keys verify", "keys auth", "models check", "models update", "models watch",
|
|
456
463
|
// Publication and release.
|
|
@@ -497,6 +504,38 @@ export const REMOTE_POLICY = new Map([
|
|
|
497
504
|
]);
|
|
498
505
|
/** How many times each invocation was classified — the contract test refuses a row stated twice. */
|
|
499
506
|
export const REMOTE_POLICY_COUNT = REMOTE_NO.length + REMOTE_STEP_UP.length + REMOTE_YES.length;
|
|
507
|
+
/**
|
|
508
|
+
* Easy-start C3: the commands a laptop's local owner may run through the terminal UI (local-owner.ts runAsLocalOwner),
|
|
509
|
+
* each with the act it is; every other row (and every command with no row) is refused to the local owner before any
|
|
510
|
+
* command code runs. Credential, people, policy and infrastructure commands are never here.
|
|
511
|
+
*/
|
|
512
|
+
const LOCAL_ALLOW = {
|
|
513
|
+
// Reads.
|
|
514
|
+
pulls: "read", graph: "read", repos: "read", status: "read", ready: "read", gaps: "read", contract: "read", "skills list": "read", "skills get": "read",
|
|
515
|
+
"task list": "read", "task show": "read", "task state": "read", "task wait": "read", "check-progress": "read", "review show": "read", welcome: "read",
|
|
516
|
+
// Acts (plan C1's list).
|
|
517
|
+
"task add": "file-task",
|
|
518
|
+
chat: "lead-send",
|
|
519
|
+
"task steer": "lead-send",
|
|
520
|
+
"task approve": "approve-plan",
|
|
521
|
+
decide: "answer-question",
|
|
522
|
+
"task stop": "stop-run",
|
|
523
|
+
"task pause": "pause-run",
|
|
524
|
+
"task hold": "pause-run",
|
|
525
|
+
"task resume": "resume-run",
|
|
526
|
+
"task continue": "resume-run",
|
|
527
|
+
"task unhold": "resume-run",
|
|
528
|
+
"task complete": "complete-task",
|
|
529
|
+
"task revise": "request-revision",
|
|
530
|
+
"task archive": "archive-task",
|
|
531
|
+
"task restore": "archive-task",
|
|
532
|
+
"repos add": "add-project",
|
|
533
|
+
};
|
|
534
|
+
/** Every guide row's local-owner mark, by invocation: `allow` with its act, else `deny`. Exported for the inventory test. */
|
|
535
|
+
export const LOCAL_POLICY = new Map(GUIDE_ROWS.map(row => [row.invocation,
|
|
536
|
+
Object.prototype.hasOwnProperty.call(LOCAL_ALLOW, row.invocation) ? { local: "allow", act: LOCAL_ALLOW[row.invocation] } : { local: "deny" }]));
|
|
537
|
+
/** The invocations LOCAL_ALLOW names, for the inventory test (one naming no row is stale). */
|
|
538
|
+
export const LOCAL_ALLOWED_INVOCATIONS = Object.keys(LOCAL_ALLOW);
|
|
500
539
|
/** Every declared command, in guide order, paired with the schema of its `--json` answer (src/contracts/cli.ts).
|
|
501
540
|
* An unclassified row is "no": nothing runs remotely by omission. */
|
|
502
541
|
export const COMMAND_ENTRIES = commandEntries(GUIDE_ROWS.map(row => ({ ...row, remote: REMOTE_POLICY.get(row.invocation) ?? "no" })));
|
package/dist/task-text.d.ts
CHANGED
|
@@ -23,3 +23,9 @@ export declare function validateTaskText(fields: {
|
|
|
23
23
|
outOfScope?: string | null;
|
|
24
24
|
touches?: readonly string[];
|
|
25
25
|
}): TaskTextRefusal | null;
|
|
26
|
+
/**
|
|
27
|
+
* A title that fits: words longer than the title limit are cut at the last whole word that fits, with an ellipsis,
|
|
28
|
+
* never mid-word (one unbroken word longer than half the limit is the only thing cut inside a word). Whitespace runs
|
|
29
|
+
* read as one space. Text within the limit is returned as it is.
|
|
30
|
+
*/
|
|
31
|
+
export declare function titleWithin(text: string, max?: number): string;
|
package/dist/task-text.js
CHANGED
|
@@ -41,3 +41,20 @@ export function validateTaskText(fields) {
|
|
|
41
41
|
const bad = validateScopeText(fields);
|
|
42
42
|
return bad === null ? null : refuse("bad-goal", bad.message);
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* A title that fits: words longer than the title limit are cut at the last whole word that fits, with an ellipsis,
|
|
46
|
+
* never mid-word (one unbroken word longer than half the limit is the only thing cut inside a word). Whitespace runs
|
|
47
|
+
* read as one space. Text within the limit is returned as it is.
|
|
48
|
+
*/
|
|
49
|
+
export function titleWithin(text, max = TASK_TEXT_LIMITS.title) {
|
|
50
|
+
if (text.length <= max)
|
|
51
|
+
return text;
|
|
52
|
+
const words = text.replace(/\s+/g, " ").trim();
|
|
53
|
+
if (words.length <= max)
|
|
54
|
+
return words;
|
|
55
|
+
const room = words.slice(0, max - 1);
|
|
56
|
+
// The room ends at a whole word when the next character is a space; else back to the last space in it.
|
|
57
|
+
const space = words[max - 1] === " " ? room.length : room.lastIndexOf(" ");
|
|
58
|
+
const cut = (space >= Math.floor(max / 2) ? room.slice(0, space) : room).replace(/[\s,;:.\-–—]+$/u, "");
|
|
59
|
+
return `${cut}…`;
|
|
60
|
+
}
|
package/dist/telegram-lead.js
CHANGED
|
@@ -285,6 +285,8 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
285
285
|
return { effects, confirmed: false, ignored: true };
|
|
286
286
|
}
|
|
287
287
|
const outcome = confirmLeadProposal(store, who, proposal.id, now, {
|
|
288
|
+
// The paired phone speaks for its account's full authority (pairing is a full session's act).
|
|
289
|
+
authority: "full",
|
|
288
290
|
via: "telegram",
|
|
289
291
|
evidenceRoot: options.evidenceRoot,
|
|
290
292
|
confirm: action.phase === "yes",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text someone else wrote, made safe to print on a terminal: the one sanitizer the live view, `peek` and the terminal
|
|
3
|
+
* UI share. Escape sequences in every form go (CSI in 7- or 8-bit form, OSC such as hyperlinks and title changes,
|
|
4
|
+
* every other ESC sequence), and with them every C0 and C1 control character (a carriage return that would
|
|
5
|
+
* overwrite a line, a backspace, a bell), every invisible format character and bidi override; a tab becomes a space.
|
|
6
|
+
* Pure: no imports, so the terminal UI's bundle carries it as is.
|
|
7
|
+
*/
|
|
8
|
+
/** One line: controls, escapes and bidi overrides out; tabs to spaces. A newline is a control here too. */
|
|
9
|
+
export declare function cleanTerminalLine(text: string): string;
|
|
10
|
+
/** Any text for the terminal UI: each line cleaned as above, its newlines kept (a lone carriage return is not one). */
|
|
11
|
+
export declare function terminalText(text: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text someone else wrote, made safe to print on a terminal: the one sanitizer the live view, `peek` and the terminal
|
|
3
|
+
* UI share. Escape sequences in every form go (CSI in 7- or 8-bit form, OSC such as hyperlinks and title changes,
|
|
4
|
+
* every other ESC sequence), and with them every C0 and C1 control character (a carriage return that would
|
|
5
|
+
* overwrite a line, a backspace, a bell), every invisible format character and bidi override; a tab becomes a space.
|
|
6
|
+
* Pure: no imports, so the terminal UI's bundle carries it as is.
|
|
7
|
+
*/
|
|
8
|
+
/** Bidi embedding, override and isolate controls. */
|
|
9
|
+
const BIDI_CONTROLS = /[--]/g;
|
|
10
|
+
/** One line: controls, escapes and bidi overrides out; tabs to spaces. A newline is a control here too. */
|
|
11
|
+
export function cleanTerminalLine(text) {
|
|
12
|
+
return text
|
|
13
|
+
// Escape sequences in every form: CSI (7- or 8-bit, any parameter bytes including ':'), OSC, and the rest.
|
|
14
|
+
.replace(/(?:\x1b\[|\u009b)[0-?]*[ -/]*[@-~]/g, "")
|
|
15
|
+
.replace(/(?:\x1b\]|\u009d)[^\x07\x1b\u009c]*(?:\x07|\x1b\\|\u009c)?/g, "")
|
|
16
|
+
.replace(/\x1b[ -/]*[0-~]/g, "")
|
|
17
|
+
.replace(/\t/g, " ")
|
|
18
|
+
.replace(/[\x00-\x1f\x7f-\x9f]/g, "")
|
|
19
|
+
// Every invisible format character (zero-width, direction marks and overrides, joiners, BOM): never shown.
|
|
20
|
+
.replace(/\p{Cf}/gu, "")
|
|
21
|
+
.replace(BIDI_CONTROLS, "");
|
|
22
|
+
}
|
|
23
|
+
/** Any text for the terminal UI: each line cleaned as above, its newlines kept (a lone carriage return is not one). */
|
|
24
|
+
export function terminalText(text) {
|
|
25
|
+
return text.split("\n").map(cleanTerminalLine).join("\n");
|
|
26
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
2
|
import { type UpdateWaiting } from "./desktop-update.js";
|
|
3
3
|
import { type SupervisorRunner } from "./daemon.js";
|
|
4
|
+
import { type ProcessProbe } from "./runner-process.js";
|
|
4
5
|
import { RUNTIME_PHASES, type RuntimeUpdateJournalRecord } from "./contracts/update-journal.js";
|
|
5
6
|
export declare const PROVENANCE_REPOSITORY = "https://github.com/ap9000/toolroll";
|
|
6
7
|
export declare const PROVENANCE_WORKFLOW = ".github/workflows/publish.yml";
|
|
@@ -226,13 +227,19 @@ type Exec = (command: string, args: string[], options?: {
|
|
|
226
227
|
stderr: string;
|
|
227
228
|
};
|
|
228
229
|
export declare function currentRuntime(version: string): RuntimeRef;
|
|
229
|
-
/** Seams for tests: npm, node and ps (`exec`), launchctl (`run`), process liveness, and launchd's waits
|
|
230
|
+
/** Seams for tests: npm, node and ps (`exec`), launchctl or systemctl (`run`), process liveness, and launchd's waits
|
|
231
|
+
* (`sleep`); for the laptop service on Linux, the platform, process identity (`probe`), the host name its process
|
|
232
|
+
* record names, and the console proof (`consoleProof`: the database's console is live at that version). */
|
|
230
233
|
export type MachineSeams = {
|
|
231
234
|
exec?: Exec;
|
|
232
235
|
run?: SupervisorRunner;
|
|
233
236
|
alive?: (pid: number) => boolean;
|
|
234
237
|
sleep?: (ms: number) => Promise<void>;
|
|
235
238
|
execPath?: string;
|
|
239
|
+
platform?: NodeJS.Platform;
|
|
240
|
+
probe?: ProcessProbe;
|
|
241
|
+
host?: string;
|
|
242
|
+
consoleProof?: (databaseFile: string, version: string) => Promise<boolean>;
|
|
236
243
|
};
|
|
237
244
|
export declare function machineSystem(home?: string, env?: Record<string, string | undefined>, seams?: MachineSeams): UpdateSystem;
|
|
238
245
|
/** npm's own Sigstore verifier. Found from npm itself (the `npm` on PATH, then the one beside node), never only from
|