flowviant 0.114.1 → 0.115.0
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/README.md +9 -0
- package/bin/lib/agentTaskKinds.mjs +10 -6
- package/bin/lib/artifacts.mjs +43 -1
- package/bin/lib/claudePersonal.mjs +145 -0
- package/bin/lib/codexPersonal.mjs +467 -0
- package/bin/lib/codexRelay.mjs +63 -0
- package/bin/lib/prompts.mjs +74 -22
- package/bin/lib/resumeLost.mjs +16 -2
- package/bin/lib/runTurn.mjs +24 -1
- package/bin/lib/runtimeClaude.mjs +44 -11
- package/bin/lib/runtimeCodex.mjs +165 -4
- package/bin/lib/runtimeEvents.mjs +9 -2
- package/bin/lib/turnProfile.mjs +3 -2
- package/bin/lib/workAgentTurnExecution.mjs +23 -3
- package/bin/lib/workSessionTurns.mjs +145 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,15 @@ Two knobs bound the blast radius, and both are worth setting on a shared box:
|
|
|
147
147
|
its permission engine is machine-wide — which is surfaced in the app rather
|
|
148
148
|
than papered over.
|
|
149
149
|
|
|
150
|
+
Fenced turns — design, 3D-model, deck and research cards, the pre-review,
|
|
151
|
+
the planner, intake, the wiki and the New task chat — read none of your Claude
|
|
152
|
+
Code settings files: their permissions and hooks are Flowviant's posture
|
|
153
|
+
alone, and of `~/.claude/settings.json` they keep only how Claude signs in
|
|
154
|
+
(`apiKeyHelper`, `env` and the like) and your model defaults. Code agents and
|
|
155
|
+
Terminal tabs still read yours, so your own allow rules and `defaultMode` add
|
|
156
|
+
to `FLOWVIANT_SAFE=1`'s allowlist there. Codex's fenced turns run with
|
|
157
|
+
`--ignore-user-config` the same way.
|
|
158
|
+
|
|
150
159
|
The posture is reported on every poll and shown in the project's
|
|
151
160
|
Settings → Machine section, so the team can see whether the box runs the
|
|
152
161
|
guarded toolset or full permissions.
|
|
@@ -33,9 +33,13 @@
|
|
|
33
33
|
* sentence a turn settles `nothing` with when the proof was not written;
|
|
34
34
|
* - `offRuntime(rt)`: the sentence when the chosen CLI cannot run the posture.
|
|
35
35
|
*/
|
|
36
|
-
/** The mockup, 3D-model
|
|
37
|
-
*
|
|
38
|
-
|
|
36
|
+
/** The mockup, 3D-model and presentation kinds run under the `design` posture,
|
|
37
|
+
* which Claude and — since 0.115.0 — Codex declare (2026-09-29, measured:
|
|
38
|
+
* runtimeCodex.mjs `codexDesignFence`). */
|
|
39
|
+
const DESIGN_CLIS = 'mockup, 3D model and presentation cards run on Claude or Codex on this machine';
|
|
40
|
+
/** The write-up kind runs on Claude alone: Codex cannot fence the web it needs
|
|
41
|
+
* apart from the whole-box reads its sandbox allows (same place). */
|
|
42
|
+
const CLAUDE_ONLY = 'write-up cards run on Claude on this machine';
|
|
39
43
|
/** The image kind runs on Codex: the one CLI that declares its posture, because
|
|
40
44
|
* the pictures come from Codex's own image tool on its own login. */
|
|
41
45
|
const CODEX_ONLY = 'image cards run on Codex on this machine';
|
|
@@ -52,7 +56,7 @@ export const AGENT_TASK_KINDS = Object.freeze({
|
|
|
52
56
|
match: /\.html?$/i,
|
|
53
57
|
missing: 'the turn ended without writing a mockup under .flowviant/artifacts/',
|
|
54
58
|
}),
|
|
55
|
-
offRuntime: () =>
|
|
59
|
+
offRuntime: () => DESIGN_CLIS,
|
|
56
60
|
}),
|
|
57
61
|
/**
|
|
58
62
|
* THE 3D-MODEL KIND (0.105.0; one page since 0.107.0): a three.js page that
|
|
@@ -69,7 +73,7 @@ export const AGENT_TASK_KINDS = Object.freeze({
|
|
|
69
73
|
match: /\.html?$/i,
|
|
70
74
|
missing: 'the turn ended without writing a 3D model page under .flowviant/artifacts/',
|
|
71
75
|
}),
|
|
72
|
-
offRuntime: () =>
|
|
76
|
+
offRuntime: () => DESIGN_CLIS,
|
|
73
77
|
}),
|
|
74
78
|
/**
|
|
75
79
|
* THE IMAGE KIND (0.114.0): one or more raster pictures, generated by
|
|
@@ -97,7 +101,7 @@ export const AGENT_TASK_KINDS = Object.freeze({
|
|
|
97
101
|
match: /\.html?$/i,
|
|
98
102
|
missing: 'the turn ended without writing a deck under .flowviant/artifacts/',
|
|
99
103
|
}),
|
|
100
|
-
offRuntime: () =>
|
|
104
|
+
offRuntime: () => DESIGN_CLIS,
|
|
101
105
|
}),
|
|
102
106
|
research: Object.freeze({
|
|
103
107
|
posture: 'research',
|
package/bin/lib/artifacts.mjs
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
* that cannot show an artifact must not have the CLI told it will.
|
|
75
75
|
*/
|
|
76
76
|
|
|
77
|
-
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync } from 'node:fs';
|
|
77
|
+
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, rmSync } from 'node:fs';
|
|
78
78
|
import { createHash } from 'node:crypto';
|
|
79
79
|
import { join } from 'node:path';
|
|
80
80
|
import { cutKeepingExtension } from './safeFileName.mjs';
|
|
@@ -138,6 +138,48 @@ export function ensureArtifactDir(placeDir) {
|
|
|
138
138
|
return real(parent) === true && real(dir) === true;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* A FENCED TURN'S OWN SCRATCH — `<place>/.flowviant/tmp`, beside the artifacts
|
|
143
|
+
* directory, under the `.flowviant/` git already ignores (0.115.0, 2026-09-29).
|
|
144
|
+
*
|
|
145
|
+
* MEASURED on codex-cli 0.156.1: under an artifacts-only profile `/tmp` is
|
|
146
|
+
* read-only, and a shell here-document needs a temp file (zsh writes it under
|
|
147
|
+
* `$TMPPREFIX`, bash and `mktemp` under `$TMPDIR`) — so `cat > page.html
|
|
148
|
+
* <<EOF` failed "can't create temp file for here document: read-only file
|
|
149
|
+
* system", left an empty page, and the turn stopped to ask why the artifacts
|
|
150
|
+
* directory was read-only. Codex's design fence grants this directory and
|
|
151
|
+
* points both variables at it (runtimeCodex.mjs `codexDesignFence`), so the
|
|
152
|
+
* shell works like a shell; nothing here is uploaded, committed or read back.
|
|
153
|
+
*
|
|
154
|
+
* Made EMPTY before each turn (a previous turn's scratch is nobody's), and
|
|
155
|
+
* only as a real directory under a real `.flowviant` — the artifacts
|
|
156
|
+
* directory's rule, for its reason: a fence grant on a path through a
|
|
157
|
+
* committed symlink is a write grant on its target. False creates nothing
|
|
158
|
+
* that matters, and the caller refuses the turn in words.
|
|
159
|
+
*/
|
|
160
|
+
export const FENCE_TMP_DIR = '.flowviant/tmp';
|
|
161
|
+
export function ensureFenceScratch(placeDir) {
|
|
162
|
+
const parent = join(placeDir, '.flowviant');
|
|
163
|
+
const dir = join(placeDir, FENCE_TMP_DIR);
|
|
164
|
+
const kind = (p) => {
|
|
165
|
+
try {
|
|
166
|
+
const st = lstatSync(p);
|
|
167
|
+
return st.isDirectory() && !st.isSymbolicLink() ? 'dir' : 'other';
|
|
168
|
+
} catch (e) {
|
|
169
|
+
return e?.code === 'ENOENT' ? 'none' : 'other';
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
if (kind(parent) !== 'dir' || kind(dir) === 'other') return false;
|
|
173
|
+
try {
|
|
174
|
+
// A real directory (checked above): its contents go, never through a link.
|
|
175
|
+
if (kind(dir) === 'dir') rmSync(dir, { recursive: true, force: true });
|
|
176
|
+
mkdirSync(dir);
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return kind(parent) === 'dir' && kind(dir) === 'dir';
|
|
181
|
+
}
|
|
182
|
+
|
|
141
183
|
export function artifactTypeFor(name) {
|
|
142
184
|
const dot = String(name).lastIndexOf('.');
|
|
143
185
|
if (dot <= 0) return null;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A FENCED CLAUDE TURN TAKES ITS PERMISSIONS AND HOOKS FROM FLOWVIANT ALONE
|
|
3
|
+
* (2026-09-29, the owner, asked whether an agent's fence should ignore the
|
|
4
|
+
* person's own permission settings: "yes, the agent's fence can ignore").
|
|
5
|
+
*
|
|
6
|
+
* A 3D-model agent under the design posture, whose whole shell is
|
|
7
|
+
* `Bash(ls:*)`, ran `google-chrome` and Playwright scripts. Every fenced lane
|
|
8
|
+
* read the person's `~/.claude/settings.json` — an agent turn through
|
|
9
|
+
* `--setting-sources user`, the pre-review, planner, intake, wiki and capture
|
|
10
|
+
* chat by naming no source at all, which ALSO reads the cwd's
|
|
11
|
+
* `.claude/settings*.json`, and the pre-review's cwd is the agent's own
|
|
12
|
+
* worktree. A settings file is a second author of the posture. MEASURED on
|
|
13
|
+
* Claude Code 2.1.284 — see claudePersonal.test.mjs for the probes and the
|
|
14
|
+
* offline check that pins them on the real CLI. In short: a personal
|
|
15
|
+
* `permissions.allow` rule admitted a command the list refused, a personal
|
|
16
|
+
* `defaultMode: "bypassPermissions"` turned the fenced turn into a bypass, and
|
|
17
|
+
* personal and worktree hooks fired; under `--setting-sources ''` none of them
|
|
18
|
+
* loaded, and the turn ran on the same login.
|
|
19
|
+
*
|
|
20
|
+
* AN EMPTY SOURCE LIST DROPS THE WHOLE USER SOURCE, not only its permissions:
|
|
21
|
+
* `~/.claude/CLAUDE.md`, personal skills and subagents went too (measured),
|
|
22
|
+
* and with the file go the plugins it enables. No flag keeps half of it — a
|
|
23
|
+
* flag setting's allow rules only add to the person's. On a fenced lane that
|
|
24
|
+
* is the right loss: the posture and the contract are Flowviant's, the base
|
|
25
|
+
* branch's instructions and skills arrive through the prompt and the reviewed
|
|
26
|
+
* plugin (`--plugin-dir` still loads, measured), and a personal plugin brings
|
|
27
|
+
* hooks of its own. Codex's fenced postures are hermetic the same way
|
|
28
|
+
* (`--ignore-user-config`, runtimeCodex.mjs).
|
|
29
|
+
*
|
|
30
|
+
* WHAT IS KEPT is what makes the CLI the person's CLI, never what it may do:
|
|
31
|
+
* how it signs in and reaches its provider, how a model the turn does not pin
|
|
32
|
+
* runs, and how long the person's own store is kept. An ALLOWLIST, so a key a
|
|
33
|
+
* later CLI adds — a sandbox auto-allow, a permission mode — never rides by
|
|
34
|
+
* default. It rides INSIDE the posture's own `--settings`, because the CLI
|
|
35
|
+
* reads one: given two, only the last one's hook fired (measured).
|
|
36
|
+
*/
|
|
37
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
38
|
+
import { homedir } from 'node:os';
|
|
39
|
+
import { join } from 'node:path';
|
|
40
|
+
import { MACHINE_CREDENTIAL_ENV } from './machineEnv.mjs';
|
|
41
|
+
import { WINDOW_ENV_REMOVED, withoutWindows } from './noWindowEnv.mjs';
|
|
42
|
+
|
|
43
|
+
/** The flag pair that loads no settings file — user, project or local.
|
|
44
|
+
* Managed (policy) settings and `--settings` still apply. */
|
|
45
|
+
export const NO_SETTING_SOURCES = Object.freeze(['--setting-sources', '']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The personal settings a fenced turn keeps, by key.
|
|
49
|
+
*
|
|
50
|
+
* · THE LOGIN — the helpers the CLI runs to authenticate and where it signs
|
|
51
|
+
* in, and `env`, where a login routes (ANTHROPIC_BASE_URL,
|
|
52
|
+
* CLAUDE_CODE_USE_BEDROCK, AWS_PROFILE). `env` gives the person's file no
|
|
53
|
+
* more reach than the operator's shell, which every turn inherits already;
|
|
54
|
+
* what a turn's environment promises is still removed from it
|
|
55
|
+
* (`keptEnv`).
|
|
56
|
+
* · HOW AN UNPINNED MODEL RUNS ("Machine default uses the CLI's own
|
|
57
|
+
* setting"): per-model effort, thinking, the model map a provider needs.
|
|
58
|
+
* `model` itself is not here: every Claude turn names `--model`, the
|
|
59
|
+
* daemon's pin when the card has none (config.mjs), so the person's
|
|
60
|
+
* default model never reached a daemon turn before either.
|
|
61
|
+
* · THE PERSON'S OWN STORE: how long transcripts live and whether memory is
|
|
62
|
+
* kept, so a fenced turn's start never prunes or writes on a default the
|
|
63
|
+
* person turned off.
|
|
64
|
+
*/
|
|
65
|
+
export const PERSONAL_KEPT = Object.freeze([
|
|
66
|
+
'apiKeyHelper',
|
|
67
|
+
'proxyAuthHelper',
|
|
68
|
+
'awsAuthRefresh',
|
|
69
|
+
'awsCredentialExport',
|
|
70
|
+
'gcpAuthRefresh',
|
|
71
|
+
'forceLoginMethod',
|
|
72
|
+
'forceLoginOrgUUID',
|
|
73
|
+
'forceLoginGatewayUrl',
|
|
74
|
+
'env',
|
|
75
|
+
'modelOverrides',
|
|
76
|
+
'modelSettings',
|
|
77
|
+
'effortLevel',
|
|
78
|
+
'maxEffortLevel',
|
|
79
|
+
'alwaysThinkingEnabled',
|
|
80
|
+
'fallbackModel',
|
|
81
|
+
'cleanupPeriodDays',
|
|
82
|
+
'autoMemoryEnabled',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
/** What a turn nobody is sitting at has set or removed (noWindowEnv.mjs) and
|
|
86
|
+
* the machine credential (machineEnv.mjs): never put back by a settings file. */
|
|
87
|
+
const TURN_OWNED_ENV = new Set([
|
|
88
|
+
...MACHINE_CREDENTIAL_ENV,
|
|
89
|
+
...WINDOW_ENV_REMOVED,
|
|
90
|
+
...Object.keys(withoutWindows({}, '/')),
|
|
91
|
+
]);
|
|
92
|
+
function keptEnv(env) {
|
|
93
|
+
if (!env || typeof env !== 'object' || Array.isArray(env)) return null;
|
|
94
|
+
const out = {};
|
|
95
|
+
for (const [k, v] of Object.entries(env)) if (typeof v === 'string' && !TURN_OWNED_ENV.has(k)) out[k] = v;
|
|
96
|
+
return Object.keys(out).length ? out : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Far past any real settings file; a bigger one is not read. */
|
|
100
|
+
const SETTINGS_MAX_BYTES = 1024 * 1024;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The person's user settings file — `$CLAUDE_CONFIG_DIR/settings.json`, else
|
|
104
|
+
* `~/.claude/settings.json`, where the CLI itself looks — reduced to
|
|
105
|
+
* PERSONAL_KEPT. `{}` when it is absent, unreadable or not a JSON object: the
|
|
106
|
+
* turn then runs on the CLI's stored login and defaults, and the fence is the
|
|
107
|
+
* same either way.
|
|
108
|
+
*/
|
|
109
|
+
export function personalClaudeSettings(env = process.env) {
|
|
110
|
+
const dir = env.CLAUDE_CONFIG_DIR || join(env.HOME || homedir(), '.claude');
|
|
111
|
+
const path = join(dir, 'settings.json');
|
|
112
|
+
let raw;
|
|
113
|
+
try {
|
|
114
|
+
if (statSync(path).size > SETTINGS_MAX_BYTES) return {};
|
|
115
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
116
|
+
} catch {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const key of PERSONAL_KEPT) {
|
|
122
|
+
if (!Object.hasOwn(raw, key) || raw[key] == null) continue;
|
|
123
|
+
if (key !== 'env') out[key] = raw[key];
|
|
124
|
+
else {
|
|
125
|
+
const kept = keptEnv(raw.env);
|
|
126
|
+
if (kept) out.env = kept;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `perm` with `kept` folded into its `--settings` — the posture's keys win, so
|
|
134
|
+
* a kept key a posture ever names is the fence's word. A posture with no
|
|
135
|
+
* `--settings` gets one before its lists (`--allowedTools` is variadic). With
|
|
136
|
+
* nothing kept, `perm` is returned as it was.
|
|
137
|
+
*/
|
|
138
|
+
export function withPersonalSettings(perm, kept) {
|
|
139
|
+
const personal = kept && typeof kept === 'object' && !Array.isArray(kept) ? kept : {};
|
|
140
|
+
if (!Object.keys(personal).length) return [...perm];
|
|
141
|
+
const at = perm.indexOf('--settings');
|
|
142
|
+
if (at < 0) return ['--settings', JSON.stringify(personal), ...perm];
|
|
143
|
+
const posture = JSON.parse(perm[at + 1]);
|
|
144
|
+
return [...perm.slice(0, at), '--settings', JSON.stringify({ ...personal, ...posture }), ...perm.slice(at + 2)];
|
|
145
|
+
}
|