flowviant 0.47.1 → 0.47.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/README.md CHANGED
@@ -68,6 +68,31 @@ Flowviant only stores the tunnel URL; your browser talks to it directly.
68
68
  | `FLOWVIANT_TOKENS=a,b,c` | a static fleet, one worktree each |
69
69
  | `FLOWVIANT_SAFE=1` | restrict the toolset instead of running unattended |
70
70
 
71
+ ## Security posture
72
+
73
+ Every project member with edit access can run turns on this machine —
74
+ Workbench tabs and @-dispatches both execute a coding agent with the daemon's
75
+ own OS permissions. Membership is the consent boundary, the same trust plane
76
+ as the shared repository: invite people you would give a shell to.
77
+
78
+ Two knobs bound the blast radius, and both are worth setting on a shared box:
79
+
80
+ - **Run the daemon under a dedicated OS user** that owns only the repository
81
+ checkout and `~/.flowviant`. This is the single biggest hardening available
82
+ — a session can then only touch that account's files, not your keys, your
83
+ home directory, or the rest of the machine. A plain separate account works;
84
+ a systemd unit with `ProtectHome=read-only` and `ReadWritePaths=` works
85
+ better.
86
+ - **`FLOWVIANT_SAFE=1`** narrows the toolset: Claude to an allowlist
87
+ (edit/read/search plus `git`/`gh`/`npm`/`bun` — no arbitrary shell), Codex
88
+ to a workspace-write sandbox. Antigravity has no per-invocation narrowing —
89
+ its permission engine is machine-wide — which is surfaced in the app rather
90
+ than papered over.
91
+
92
+ The posture is reported on every poll and shown in the project's
93
+ Settings → Machine section, so the team can see whether the box runs the
94
+ guarded toolset or full permissions.
95
+
71
96
  ## License
72
97
 
73
98
  MIT — see [LICENSE](./LICENSE).
package/bin/lib/fleet.mjs CHANGED
@@ -101,6 +101,12 @@ async function fetchRoster(haveIds) {
101
101
  // our own package.json). Older servers ignore unknown params, so sending it
102
102
  // unconditionally is always safe.
103
103
  url.searchParams.set('dv', VERSION);
104
+ // The permission posture this machine runs turns under — '1' when
105
+ // FLOWVIANT_SAFE narrows the toolset, '0' when everything is granted. A
106
+ // statement of configuration, not a request: the app SHOWS it in Settings
107
+ // so a team can see whether the shared box runs wide open, and enforces
108
+ // nothing (membership is the consent boundary). Older servers ignore it.
109
+ url.searchParams.set('safe', SAFE ? '1' : '0');
104
110
  // WHICH CLIs this machine actually has, so the app can stop guessing.
105
111
  //
106
112
  // Until now every surface that listed Gemini or Codex said "not wired up yet"
@@ -458,26 +458,40 @@ MECHANICS OF THIS TAB:
458
458
  4. NEVER merge to main, deploy, or force-push unless the human explicitly says
459
459
  so in this conversation. Branch pushes and PRs are fine when asked. Shipping
460
460
  is their word to say, not yours to infer.
461
+ 5. WHEN THEY HAVE TO CHOOSE, HAND THEM THE CHOICES. A real pick between known
462
+ options — not an open question — ends your reply with a fenced block the app
463
+ renders as buttons; their click composes their answer as the next message:
464
+
465
+ \`\`\`flowviant-ask
466
+ {"question": "Which auth flow?", "options": ["Magic link", "Password", "Both"], "multiSelect": false}
467
+ \`\`\`
468
+
469
+ ONE block per reply, and always the LAST thing in it. Two to eight options,
470
+ each label short enough to sit on a button. multiSelect true only for a
471
+ genuine check-several-of-these case. NEVER for an open question — ask those
472
+ in prose, like anyone would. And ask the question in prose above the block
473
+ as well: a client that doesn't render the fence shows it as plain text, so
474
+ the reply has to read as a question with its options either way.
461
475
 
462
476
  THE LEDGER. This session's work is logged as CARDS as it happens, by you,
463
477
  through tools — so a four-hour churn doesn't evaporate into scrollback. The
464
478
  rules:
465
479
 
466
- 5. CLAIM WHAT YOU WORK. When they say "take the auth card" or "next", call
480
+ 6. CLAIM WHAT YOU WORK. When they say "take the auth card" or "next", call
467
481
  list_cards, then claim_card the one they mean. The card you hold is the
468
482
  tab's "Now" — it is how they and their team see what this session is doing.
469
- 6. LOG DRIFT, don't ask permission for it. "Also fix that redirect" mid-flow:
483
+ 7. LOG DRIFT, don't ask permission for it. "Also fix that redirect" mid-flow:
470
484
  do the work, and file_card it — check list_cards FIRST; if a planned card
471
485
  already covers it, claim that one instead of filing a twin. One card per
472
486
  shippable unit. Never card-ify chatter, questions, or exploration.
473
- 7. DELIVER WITH RECEIPTS. When a card's work is committed, deliver_card with a
487
+ 8. DELIVER WITH RECEIPTS. When a card's work is committed, deliver_card with a
474
488
  one-paragraph summary and the commit shas. Delivered is ASSERTED; done is
475
489
  OBSERVED (the merge, on their word). Never claim done, and never deliver
476
490
  work that isn't committed.
477
- 8. RAISE WHAT YOU SPOT. A design flaw, a follow-up they named for later —
491
+ 9. RAISE WHAT YOU SPOT. A design flaw, a follow-up they named for later —
478
492
  raise_card, queued, unheld. You do not start raised work.
479
- 9. BE PROPORTIONAL. A one-line typo fix inside the card you already hold is
480
- that card's work, not a new card. When in doubt, fewer cards.
493
+ 10. BE PROPORTIONAL. A one-line typo fix inside the card you already hold is
494
+ that card's work, not a new card. When in doubt, fewer cards.
481
495
 
482
496
  POSTURE: terminal, not ticket. Don't ask permission to look at things. Don't
483
497
  narrate ceremony. Ground claims in files you opened. When they ask a question,
@@ -514,6 +528,21 @@ MECHANICS OF THIS TAB:
514
528
  4. NEVER merge to main, deploy, or force-push unless the human explicitly says
515
529
  so in this conversation. Branch pushes are fine when asked. Shipping is
516
530
  their word to say, not yours to infer.
531
+ 5. WHEN THEY HAVE TO CHOOSE, HAND THEM THE CHOICES. You have no tools here, but
532
+ this one costs none — it is text. A real pick between known options (not an
533
+ open question) ends your reply with a fenced block the app renders as
534
+ buttons; their click composes their answer as the next message:
535
+
536
+ \`\`\`flowviant-ask
537
+ {"question": "Which auth flow?", "options": ["Magic link", "Password", "Both"], "multiSelect": false}
538
+ \`\`\`
539
+
540
+ ONE block per reply, and always the LAST thing in it. Two to eight options,
541
+ each label short enough to sit on a button. multiSelect true only for a
542
+ genuine check-several-of-these case. NEVER for an open question — ask those
543
+ in prose, like anyone would. And ask the question in prose above the block
544
+ as well: a client that doesn't render the fence shows it as plain text, so
545
+ the reply has to read as a question with its options either way.
517
546
 
518
547
  POSTURE: terminal, not ticket. Don't ask permission to look at things. Ground
519
548
  claims in files you opened. When they ask a question, answer it; when they ask
package/bin/lib/work.mjs CHANGED
@@ -45,6 +45,45 @@ import { detectRuntimes, canRun, RUNTIMES } from './runtimes.mjs';
45
45
  import { isTerminalSessionLive, isAgyConversationLive } from './localSessions.mjs';
46
46
  import { homedir } from 'node:os';
47
47
 
48
+ /**
49
+ * The shape a per-tab model name must have before it rides argv as
50
+ * `--model <name>`. Conservative for the same reason the codex thread id is
51
+ * (below): it comes off the wire and lands in a child process's arguments —
52
+ * alphanumerics plus dot/dash/underscore, at most 40 characters, and NEVER a
53
+ * leading dash, which is an argv that parses as a flag.
54
+ */
55
+ const WORK_MODEL_RE = /^[a-zA-Z0-9._][a-zA-Z0-9._-]{0,39}$/;
56
+
57
+ /** The five efforts the CLIs actually accept. A literal set rather than a
58
+ * pattern: there is no such thing as an effort we haven't heard of, and the
59
+ * server's own union is exactly this list. */
60
+ const WORK_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
61
+
62
+ /**
63
+ * WHICH BRAIN, AT WHICH EFFORT — the tab's own pick, off the roster.
64
+ *
65
+ * Absent is the resting state and it must stay genuinely absent: every tab ran
66
+ * with no `--model` and no `--effort` until now, so a job that names neither
67
+ * has to produce the byte-identical argv it produced yesterday — Claude falling
68
+ * back to the machine's MODEL pin, codex and agy to their own defaults. Hence
69
+ * an object with the key MISSING rather than one holding null: a null would
70
+ * reach the builders as a value and Claude's `model || MODEL` is the only one
71
+ * that would survive it.
72
+ *
73
+ * A value that fails its guard is DROPPED, not passed through and not an error.
74
+ * The honest outcome of "the server named a model this machine can't spell" is
75
+ * the machine's own default — a turn that runs — rather than a flag no CLI
76
+ * understands and a tab that fails every message.
77
+ */
78
+ function brainFor(job) {
79
+ const out = {};
80
+ const model = typeof job?.model === 'string' ? job.model.trim() : '';
81
+ if (model && WORK_MODEL_RE.test(model)) out.model = model;
82
+ const effort = typeof job?.effort === 'string' ? job.effort.trim() : '';
83
+ if (effort && WORK_EFFORTS.has(effort)) out.effort = effort;
84
+ return out;
85
+ }
86
+
48
87
  export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLeaseTtl }) {
49
88
  const WORK_TOKEN_URL = FLEET_URL.replace(/\/agents\/?$/, '/work-token');
50
89
  const WORK_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/work-turn-done');
@@ -879,6 +918,10 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
879
918
  const mcp = plainTab
880
919
  ? { args: [], env: null, dir: null }
881
920
  : mcpFor(rt.id, mint.token, getMcpUrl());
921
+ // The tab's model/effort, if it named any. Spread into turnArgs so
922
+ // BOTH runTurn calls below carry it — the retry is the same turn on
923
+ // the same brain, not a quieter second opinion.
924
+ const brain = brainFor(job);
882
925
  // Attempts count RUNS: the infra refusals above consumed nothing and
883
926
  // settled on their own terms.
884
927
  workAttempts.set(job.id, tries + 1);
@@ -908,6 +951,8 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
908
951
  // ordinary --continue resume path, unchanged.
909
952
  ...(adopting ? { adoptResumeId: job.adopt.id } : {}),
910
953
  system: plainTab ? SYSTEM_WORK_PLAIN : SYSTEM_WORK,
954
+ // Present only when the tab named one — see brainFor.
955
+ ...brain,
911
956
  cwd: dir.wt,
912
957
  mcpArgs: mcp.args,
913
958
  mcpEnv: mcp.env,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.47.1",
3
+ "version": "0.47.2",
4
4
  "description": "Run your own coding CLIs as headless build agents for Flowviant — Claude Code or Codex, on your own credentials. Claims dispatched work, opens PRs, captures review evidence, and routes questions back to you.",
5
5
  "type": "module",
6
6
  "bin": {