@brainervirus/workit-core 8.19.0 → 8.19.2
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/package.json
CHANGED
|
@@ -24,7 +24,10 @@ description: Build a requested change in small verified steps - follow local pat
|
|
|
24
24
|
lease waits and names its holder (`workit lock status`).
|
|
25
25
|
5. Prove the feature on its real surface with the project's `verify-<app>`
|
|
26
26
|
skill; none yet? Write one (workit-verify-app). Tests show branch behavior,
|
|
27
|
-
not that the feature works.
|
|
27
|
+
not that the feature works. The report carries what you observed: a UI
|
|
28
|
+
change a screenshot or console/network check (chrome devtools or
|
|
29
|
+
agent-browser), an API a quoted real response. App not runnable? Say so
|
|
30
|
+
plainly, never claim success.
|
|
28
31
|
6. Commit: `workit git commit -m "<type>: <what>" -- <paths>` (or `--all`).
|
|
29
32
|
No endpoint named? Stop here and state the next command. Hand delivery to
|
|
30
33
|
ship, which pushes and opens the PR with its body shape (workit-ship), only
|
|
@@ -55,8 +58,7 @@ flag was never invoked)
|
|
|
55
58
|
|
|
56
59
|
Good: "Added `--since`. measured: `workit check test` exit 0;
|
|
57
60
|
`mytool log --since 2d` printed 3 entries against the fixture repo. inferred:
|
|
58
|
-
the GitLab path behaves the same (
|
|
59
|
-
to the verifier agent."
|
|
61
|
+
the GitLab path behaves the same (not run). Handed to the verifier agent."
|
|
60
62
|
|
|
61
63
|
## Check
|
|
62
64
|
|
|
@@ -27,7 +27,10 @@ way a user would.
|
|
|
27
27
|
- **Drive:** how to exercise it: CLI invocations, `curl` against routes, a
|
|
28
28
|
browser tool for UI, with fixture data.
|
|
29
29
|
- **Evidence:** what to capture (exit code, response body, screenshot path)
|
|
30
|
-
and the command that records it.
|
|
30
|
+
and the command that records it. A UI is driven with chrome devtools or
|
|
31
|
+
agent-browser, with a screenshot or console/network observation; an API
|
|
32
|
+
with a real call, response quoted. The report carries the observation.
|
|
33
|
+
App not runnable? Say so plainly, never claim success.
|
|
31
34
|
- **Cleanup:** stop processes, remove temp data.
|
|
32
35
|
- **Feature map:** feature, how to drive it, expected observation.
|
|
33
36
|
3. **Prove it end-to-end once:** launch, drive one feature, capture evidence,
|
|
@@ -54,7 +57,8 @@ report `clean`, `changed` (what) or `blocked` (why).
|
|
|
54
57
|
Bad: "Verify by running the tests." (that is not the real surface)
|
|
55
58
|
|
|
56
59
|
Good: "Launch `bun run dev` (ready: `listening on :5173`, 30 s). Drive:
|
|
57
|
-
`curl -s localhost:5173/api/health`
|
|
60
|
+
`curl -s localhost:5173/api/health` returned `{"ok":true}`; screenshot of
|
|
61
|
+
/settings at .workit-scratch/settings.png. Evidence:
|
|
58
62
|
`workit check --name verify-web -- bun scripts/smoke.ts` exit 0."
|
|
59
63
|
|
|
60
64
|
## Check
|
|
@@ -30,6 +30,8 @@ description: Launch, drive and observe <app> on its real surface (<cli|web|api>)
|
|
|
30
30
|
## Evidence
|
|
31
31
|
`workit check --name verify-<app> -- <driver command>` records exit code and log.
|
|
32
32
|
Screenshots go to `<tmp dir>/verify-<app>/<feature>.png`; cite the path.
|
|
33
|
+
UI: also report a console or network observation. API: quote the real response.
|
|
34
|
+
If the app cannot be run, say so plainly instead of claiming success.
|
|
33
35
|
|
|
34
36
|
## Cleanup
|
|
35
37
|
`<command>` (stop servers, remove `<tmp dir>`).
|
package/src/core/heavy-words.ts
CHANGED
|
@@ -32,7 +32,7 @@ const wordsOf = (command: string): string[] =>
|
|
|
32
32
|
)
|
|
33
33
|
.filter(Boolean);
|
|
34
34
|
|
|
35
|
-
const stateDir = (env: NodeJS.ProcessEnv): string => {
|
|
35
|
+
export const stateDir = (env: NodeJS.ProcessEnv): string => {
|
|
36
36
|
if (env.WORKFLOW_TOOLKIT_STATE) return env.WORKFLOW_TOOLKIT_STATE;
|
|
37
37
|
if (env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "workit");
|
|
38
38
|
const home = env.HOME || os.homedir();
|
package/src/hooks/write-gate.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type LiveDeps,
|
|
18
18
|
type LiveOwner,
|
|
19
19
|
} from "../core/live-session";
|
|
20
|
+
import { resolveStateDir } from "../core/logger";
|
|
20
21
|
import { workspaceLiveSessionGuard } from "../core/workspaces";
|
|
21
22
|
import { appendHookObserved, ledgerPath } from "../ledger";
|
|
22
23
|
import type { HookDecision } from "./protocol";
|
|
@@ -41,7 +42,8 @@ const canonical = (file: string): string => {
|
|
|
41
42
|
let rest = "";
|
|
42
43
|
for (let dir = path.resolve(file); ; dir = path.dirname(dir)) {
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
+
// Native resolution also expands Windows 8.3 short names (RUNNER~1).
|
|
46
|
+
return path.join(realpathSync.native(dir), rest);
|
|
45
47
|
} catch {
|
|
46
48
|
if (path.dirname(dir) === dir) return path.resolve(file);
|
|
47
49
|
rest = rest ? path.join(path.basename(dir), rest) : path.basename(dir);
|
|
@@ -325,9 +327,10 @@ const POPD = new Set(["popd", "pop-location"]);
|
|
|
325
327
|
|
|
326
328
|
/** A `cd` operand resolved against `dir`; undefined when it cannot be known. */
|
|
327
329
|
const cdInto = (dir: string | undefined, words: string[]): string | undefined => {
|
|
328
|
-
const
|
|
329
|
-
// Only a leading `~` expands; a mid-path `~` is literal (Windows 8.3
|
|
330
|
+
const named = words.slice(1).find((word) => !word.startsWith("-") || word === "-");
|
|
331
|
+
// Only a leading `~` or `$HOME` expands; a mid-path `~` is literal (Windows 8.3
|
|
330
332
|
// short names like RUNNER~1).
|
|
333
|
+
const target = named === undefined ? undefined : homeExpanded(named);
|
|
331
334
|
if (dir === undefined || target === "-" || (target && /^~|[$`*?]/.test(target))) return undefined;
|
|
332
335
|
return target === undefined ? os.homedir() : path.resolve(dir, target);
|
|
333
336
|
};
|
|
@@ -335,6 +338,12 @@ const cdInto = (dir: string | undefined, words: string[]): string | undefined =>
|
|
|
335
338
|
const quoteWord = (word: string): string =>
|
|
336
339
|
/^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
|
|
337
340
|
|
|
341
|
+
/** A leading `~`, `$HOME` or `${HOME}` the shell would expand. */
|
|
342
|
+
const homeExpanded = (file: string): string => {
|
|
343
|
+
const match = /^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/.exec(file);
|
|
344
|
+
return match ? (process.env.HOME || os.homedir()) + file.slice(match[0].length) : file;
|
|
345
|
+
};
|
|
346
|
+
|
|
338
347
|
/**
|
|
339
348
|
* Per top-level segment, the files it writes (redirects, `touch`, `rm`,
|
|
340
349
|
* `sed -i`…) and `workit git commit|branch`, resolved against the
|
|
@@ -351,8 +360,10 @@ const segmentTargets = (
|
|
|
351
360
|
let dir: string | undefined = cwd;
|
|
352
361
|
let stack: Array<string | undefined> = [];
|
|
353
362
|
const groups: Array<{ dir: string | undefined; stack: Array<string | undefined> }> = [];
|
|
354
|
-
const at = (file: string) =>
|
|
355
|
-
|
|
363
|
+
const at = (file: string) => {
|
|
364
|
+
const home = homeExpanded(file);
|
|
365
|
+
return path.isAbsolute(home) ? home : dir === undefined ? null : path.resolve(dir, home);
|
|
366
|
+
};
|
|
356
367
|
for (const segment of segmentsOf(command, dialect)) {
|
|
357
368
|
if (segment.group === "open") {
|
|
358
369
|
groups.push({ dir, stack: [...stack] });
|
|
@@ -474,6 +485,65 @@ const claimAll = (owner: LiveOwner, targets: readonly string[], deps?: LiveDeps)
|
|
|
474
485
|
const liveTargets = (cwd: string, paths: readonly string[] | null): string[] =>
|
|
475
486
|
paths?.length ? paths.map((file) => path.resolve(cwd, file)) : [path.join(cwd, DIR_TARGET)];
|
|
476
487
|
|
|
488
|
+
// --- the issued-registry guard (#292) ---------------------------------------
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* The files an agent tool call writes under `<state>/issued*`: the issued-id
|
|
492
|
+
* records and their token hashes. Only the hook and the CLI write there; an
|
|
493
|
+
* agent that could would forge an issued session or token by hand.
|
|
494
|
+
*/
|
|
495
|
+
const issuedTarget = (cwd: string, targets: readonly string[]): string | null => {
|
|
496
|
+
const state = resolveStateDir();
|
|
497
|
+
const found = targets.find((target) =>
|
|
498
|
+
underIssued(state, cwd, homeExpanded(target), process.platform, canonical),
|
|
499
|
+
);
|
|
500
|
+
return found ?? null;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Whether `target` (resolved against `cwd`) is `<state>/issued*` or inside
|
|
505
|
+
* it. Pure over `platform`, so Windows shapes are testable anywhere: mixed
|
|
506
|
+
* `\` and `/`, drive-letter case, and the `/c/Users/...` form a POSIX shell
|
|
507
|
+
* on Windows uses. `real` resolves symlinks and 8.3 short names (RUNNER~1) on
|
|
508
|
+
* the live platform.
|
|
509
|
+
*/
|
|
510
|
+
export const underIssued = (
|
|
511
|
+
state: string,
|
|
512
|
+
cwd: string,
|
|
513
|
+
target: string,
|
|
514
|
+
platform: NodeJS.Platform = process.platform,
|
|
515
|
+
real: (file: string) => string = (file) => file,
|
|
516
|
+
): boolean => {
|
|
517
|
+
const win = platform === "win32";
|
|
518
|
+
const p = win ? path.win32 : path.posix;
|
|
519
|
+
const fold = (value: string) => (win || platform === "darwin" ? value.toLowerCase() : value);
|
|
520
|
+
const named = win ? target.replace(/^\/([a-zA-Z])(?=\/|$)/, "$1:") : target;
|
|
521
|
+
const relative = p.relative(fold(real(p.resolve(state))), fold(real(p.resolve(cwd, named))));
|
|
522
|
+
if (relative.startsWith("..") || p.isAbsolute(relative)) return false;
|
|
523
|
+
return relative.split(p.sep)[0].startsWith("issued");
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* A POSIX shell drops an unquoted `\` before the analysis sees the words, so
|
|
528
|
+
* `> C:\state\issued\x` loses its separators. On Windows, a command that
|
|
529
|
+
* writes and spells out the registry path is denied.
|
|
530
|
+
*/
|
|
531
|
+
const unquotedWindowsIssued = (command: string): boolean => {
|
|
532
|
+
if (process.platform !== "win32" || !command.includes("\\")) return false;
|
|
533
|
+
const flat = (value: string) => value.replaceAll("\\", "/").toLowerCase();
|
|
534
|
+
return flat(command).includes(`${flat(resolveStateDir())}/issued`) && shellWrites(command).writes;
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const issuedDeny = (file: string): HookDecision => {
|
|
538
|
+
const unblock =
|
|
539
|
+
"Issued ids and tokens are written only by Workit's SubagentStart hook. Run the verifier or reviewer agent so the host issues its own session; never edit the issued registries by hand.";
|
|
540
|
+
return {
|
|
541
|
+
kind: "deny",
|
|
542
|
+
reason: `workit issued-registry guard: ${file} is under Workit's issued-session state. ${unblock}`,
|
|
543
|
+
unblock,
|
|
544
|
+
};
|
|
545
|
+
};
|
|
546
|
+
|
|
477
547
|
/**
|
|
478
548
|
* Deny a working-tree edit while the branch task has unmet before-write
|
|
479
549
|
* requirements. `paths` are the files the tool writes, or null when unknown
|
|
@@ -488,6 +558,8 @@ export function writeGate(
|
|
|
488
558
|
owner?: LiveOwner | null,
|
|
489
559
|
deps?: LiveDeps,
|
|
490
560
|
): HookDecision {
|
|
561
|
+
const forged = paths?.length ? issuedTarget(cwd, paths) : null;
|
|
562
|
+
if (forged) return issuedDeny(forged);
|
|
491
563
|
if (!owner) return beforeWriteGate(cwd, paths);
|
|
492
564
|
const targets = liveTargets(cwd, paths);
|
|
493
565
|
const live = liveGate(owner, targets, deps);
|
|
@@ -511,6 +583,14 @@ export function shellGate(
|
|
|
511
583
|
dialect: ShellDialect = "posix",
|
|
512
584
|
deps?: LiveDeps,
|
|
513
585
|
): HookDecision {
|
|
586
|
+
try {
|
|
587
|
+
const forged =
|
|
588
|
+
issuedTarget(cwd, segmentTargets(cwd, command, dialect)) ??
|
|
589
|
+
(unquotedWindowsIssued(command) ? "the issued registry" : null);
|
|
590
|
+
if (forged) return issuedDeny(forged);
|
|
591
|
+
} catch {
|
|
592
|
+
// Fail open, like the other shell analyses here.
|
|
593
|
+
}
|
|
514
594
|
const writes = shellWrites(command);
|
|
515
595
|
let targets: string[] = [];
|
|
516
596
|
if (owner) {
|