@venturewild/workspace 0.4.2 → 0.5.1

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.
Files changed (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +112 -112
  3. package/package.json +85 -83
  4. package/server/bin/wild-workspace.mjs +1096 -1096
  5. package/server/src/account.mjs +114 -114
  6. package/server/src/agent-login.mjs +146 -146
  7. package/server/src/agent-readiness.mjs +200 -200
  8. package/server/src/agent.mjs +468 -468
  9. package/server/src/bazaar/core.mjs +790 -579
  10. package/server/src/bazaar/index.mjs +88 -75
  11. package/server/src/bazaar/mcp-server.mjs +417 -328
  12. package/server/src/bazaar/mock-tickup.mjs +97 -97
  13. package/server/src/bazaar/preview-server.mjs +95 -95
  14. package/server/src/bazaar/seed-recipes/customer-feedback-form/know-how.md +23 -23
  15. package/server/src/bazaar/seed-recipes/customer-feedback-form/recipe.json +24 -24
  16. package/server/src/bazaar/seed-recipes/landing-page-launch/know-how.md +29 -29
  17. package/server/src/bazaar/seed-recipes/landing-page-launch/recipe.json +25 -25
  18. package/server/src/bazaar/seed-recipes/personal-portfolio/know-how.md +21 -21
  19. package/server/src/bazaar/seed-recipes/personal-portfolio/recipe.json +24 -24
  20. package/server/src/bazaar/seed-recipes/receipt-sorter/know-how.md +31 -31
  21. package/server/src/bazaar/seed-recipes/receipt-sorter/recipe.json +25 -25
  22. package/server/src/bazaar/seed-recipes/tickup-hr-matching/know-how.md +79 -79
  23. package/server/src/bazaar/seed-recipes/tickup-hr-matching/recipe.json +40 -32
  24. package/server/src/canvas/core.mjs +446 -446
  25. package/server/src/canvas/index.mjs +42 -42
  26. package/server/src/canvas/mcp-server.mjs +253 -253
  27. package/server/src/canvas-rails.mjs +108 -108
  28. package/server/src/config.mjs +404 -404
  29. package/server/src/daemon-bin.mjs +110 -110
  30. package/server/src/daemon-supervisor.mjs +285 -285
  31. package/server/src/doctor.mjs +375 -375
  32. package/server/src/inbox.mjs +86 -86
  33. package/server/src/index.mjs +3279 -3032
  34. package/server/src/listings-rails.mjs +126 -0
  35. package/server/src/logpaths.mjs +98 -98
  36. package/server/src/observability.mjs +45 -45
  37. package/server/src/operator.mjs +92 -92
  38. package/server/src/pairing.mjs +137 -137
  39. package/server/src/service.mjs +515 -515
  40. package/server/src/session-reporter.mjs +201 -201
  41. package/server/src/settings.mjs +145 -145
  42. package/server/src/share.mjs +182 -182
  43. package/server/src/skills.mjs +213 -213
  44. package/server/src/supervisor.mjs +647 -647
  45. package/server/src/support-consent.mjs +133 -133
  46. package/server/src/sync.mjs +248 -248
  47. package/server/src/transcript.mjs +121 -121
  48. package/server/src/turn-mcp.mjs +46 -46
  49. package/server/src/usage.mjs +405 -405
  50. package/server/src/workspace-registry.mjs +295 -295
  51. package/server/src/workspaces.mjs +145 -135
  52. package/web/dist/assets/index-B8tHt7x-.css +32 -0
  53. package/web/dist/assets/index-BRY-IKaC.js +131 -0
  54. package/web/dist/index.html +2 -2
  55. package/web/dist/assets/index-DahRXN26.js +0 -91
  56. package/web/dist/assets/index-NXZN2LU2.css +0 -1
@@ -1,133 +1,133 @@
1
- // Support-channel consent (Phase 3, Pillar E) — the user's "VentureWild support
2
- // may act on my machine" grant for the daemon-hosted support channel (Pillar A).
3
- //
4
- // SECURITY POSTURE: OFF by default + PER-INCIDENT + TIME-BOXED (Tuan's locked
5
- // decisions, design doc Part 6). A grant is an explicit user gesture
6
- // (`wild-workspace support allow`), carries a tier (1 = read-only diagnostics/
7
- // logs; 2 = curated fixes) and an expiry, and auto-expires. Revoke is instant:
8
- // delete the file (`wild-workspace support revoke`).
9
- //
10
- // CRITICAL — lives in the machine-global dir (`~/.wild-workspace`), NOT the
11
- // per-workspace dataDir, because the **bmo-sync daemon** (a separate process) is
12
- // the enforcement gate and reads this exact file by the same `~/.wild-workspace`
13
- // convention. The relay never stores or checks consent — so a relay compromise
14
- // can't forge it; the daemon's local read is the hard gate. (Contrast
15
- // operator.mjs, which is server-read and keyed on dataDir.)
16
-
17
- import fs from 'node:fs';
18
- import path from 'node:path';
19
- import crypto from 'node:crypto';
20
-
21
- export const SUPPORT_CONSENT_VERSION = 1;
22
- // Tiers: 1 = read-only diagnostics/logs · 2 = curated fixes (restart/relink/
23
- // reinstall) · 3 = agent-mediated operation (Phase 4 — support drives the agent
24
- // on a task) · 4 = raw shell (the rare, loud escape hatch; Phase 4 PR 4.4).
25
- // Tiers 3–4 are "operate" grants and the CLI requires explicit confirmation.
26
- export const MAX_TIER = 4;
27
- /** At/above this tier a grant lets support OPERATE the machine (agent or shell). */
28
- export const OPERATE_TIER = 3;
29
- /** Cap a single grant so a forgotten "allow" can't leave the door open forever. */
30
- export const MAX_GRANT_MINUTES = 24 * 60;
31
-
32
- export function consentFile(globalDir) {
33
- return path.join(globalDir, 'support-consent.json');
34
- }
35
-
36
- /** The raw consent record, or null when absent/unparseable. No expiry check. */
37
- export function loadConsent(globalDir) {
38
- try {
39
- const p = JSON.parse(fs.readFileSync(consentFile(globalDir), 'utf8'));
40
- const tier = Number(p.tier);
41
- const expiresAt = Number(p.expiresAt);
42
- if (!Number.isFinite(tier) || !Number.isFinite(expiresAt)) return null;
43
- return {
44
- tier,
45
- grantedAt: Number(p.grantedAt) || null,
46
- expiresAt,
47
- nonce: typeof p.nonce === 'string' ? p.nonce : null,
48
- version: Number(p.version) || SUPPORT_CONSENT_VERSION,
49
- };
50
- } catch {
51
- return null;
52
- }
53
- }
54
-
55
- /**
56
- * Effective consent status, with the expiry applied. `enabled` is true only when
57
- * a grant exists AND hasn't expired. `now` is injectable for tests.
58
- */
59
- export function consentStatus(globalDir, { now = Date.now } = {}) {
60
- const rec = loadConsent(globalDir);
61
- const t = now();
62
- if (!rec) return { enabled: false, tier: 0, expiresAt: null, remainingMs: 0, file: consentFile(globalDir) };
63
- const remainingMs = Math.max(0, rec.expiresAt - t);
64
- const enabled = remainingMs > 0;
65
- return {
66
- enabled,
67
- tier: enabled ? rec.tier : 0,
68
- grantedAt: rec.grantedAt,
69
- expiresAt: rec.expiresAt,
70
- remainingMs,
71
- file: consentFile(globalDir),
72
- };
73
- }
74
-
75
- /**
76
- * Grant support consent at `tier` for `minutes`. Overwrites any prior grant (a
77
- * fresh, explicit gesture). Returns the stored record, or null if it couldn't be
78
- * persisted. `minutes` is clamped to (0, MAX_GRANT_MINUTES]; `tier` to [1, MAX_TIER].
79
- */
80
- export function grantConsent(globalDir, { tier = 1, minutes = 60, now = Date.now } = {}) {
81
- const clampedTier = Math.min(MAX_TIER, Math.max(1, Math.floor(Number(tier) || 1)));
82
- const clampedMinutes = Math.min(MAX_GRANT_MINUTES, Math.max(1, Math.floor(Number(minutes) || 1)));
83
- const grantedAt = now();
84
- const rec = {
85
- tier: clampedTier,
86
- grantedAt,
87
- expiresAt: grantedAt + clampedMinutes * 60_000,
88
- nonce: crypto.randomBytes(12).toString('base64url'),
89
- version: SUPPORT_CONSENT_VERSION,
90
- };
91
- try {
92
- fs.mkdirSync(globalDir, { recursive: true });
93
- fs.writeFileSync(consentFile(globalDir), JSON.stringify(rec, null, 2), { mode: 0o600 });
94
- } catch {
95
- return null;
96
- }
97
- return rec;
98
- }
99
-
100
- /** Revoke instantly by deleting the file. Returns true if a grant was removed. */
101
- export function revokeConsent(globalDir) {
102
- try {
103
- fs.rmSync(consentFile(globalDir));
104
- return true;
105
- } catch {
106
- return false;
107
- }
108
- }
109
-
110
- export function auditFile(globalDir) {
111
- return path.join(globalDir, 'support-audit.jsonl');
112
- }
113
-
114
- /**
115
- * The user-owned support audit: the most recent `limit` actions the daemon
116
- * recorded (newest first). Written by the bmo-sync daemon (one JSON object per
117
- * line) into the same machine-global dir; this is the read side the UI/CLI shows.
118
- * Best-effort — a missing/corrupt file yields an empty list.
119
- */
120
- export function readAudit(globalDir, { limit = 50 } = {}) {
121
- let raw;
122
- try {
123
- raw = fs.readFileSync(auditFile(globalDir), 'utf8');
124
- } catch {
125
- return [];
126
- }
127
- const lines = raw.split('\n').filter(Boolean);
128
- const out = [];
129
- for (const line of lines.slice(-limit)) {
130
- try { out.push(JSON.parse(line)); } catch { /* skip a partial/corrupt line */ }
131
- }
132
- return out.reverse(); // newest first
133
- }
1
+ // Support-channel consent (Phase 3, Pillar E) — the user's "VentureWild support
2
+ // may act on my machine" grant for the daemon-hosted support channel (Pillar A).
3
+ //
4
+ // SECURITY POSTURE: OFF by default + PER-INCIDENT + TIME-BOXED (Tuan's locked
5
+ // decisions, design doc Part 6). A grant is an explicit user gesture
6
+ // (`wild-workspace support allow`), carries a tier (1 = read-only diagnostics/
7
+ // logs; 2 = curated fixes) and an expiry, and auto-expires. Revoke is instant:
8
+ // delete the file (`wild-workspace support revoke`).
9
+ //
10
+ // CRITICAL — lives in the machine-global dir (`~/.wild-workspace`), NOT the
11
+ // per-workspace dataDir, because the **bmo-sync daemon** (a separate process) is
12
+ // the enforcement gate and reads this exact file by the same `~/.wild-workspace`
13
+ // convention. The relay never stores or checks consent — so a relay compromise
14
+ // can't forge it; the daemon's local read is the hard gate. (Contrast
15
+ // operator.mjs, which is server-read and keyed on dataDir.)
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import crypto from 'node:crypto';
20
+
21
+ export const SUPPORT_CONSENT_VERSION = 1;
22
+ // Tiers: 1 = read-only diagnostics/logs · 2 = curated fixes (restart/relink/
23
+ // reinstall) · 3 = agent-mediated operation (Phase 4 — support drives the agent
24
+ // on a task) · 4 = raw shell (the rare, loud escape hatch; Phase 4 PR 4.4).
25
+ // Tiers 3–4 are "operate" grants and the CLI requires explicit confirmation.
26
+ export const MAX_TIER = 4;
27
+ /** At/above this tier a grant lets support OPERATE the machine (agent or shell). */
28
+ export const OPERATE_TIER = 3;
29
+ /** Cap a single grant so a forgotten "allow" can't leave the door open forever. */
30
+ export const MAX_GRANT_MINUTES = 24 * 60;
31
+
32
+ export function consentFile(globalDir) {
33
+ return path.join(globalDir, 'support-consent.json');
34
+ }
35
+
36
+ /** The raw consent record, or null when absent/unparseable. No expiry check. */
37
+ export function loadConsent(globalDir) {
38
+ try {
39
+ const p = JSON.parse(fs.readFileSync(consentFile(globalDir), 'utf8'));
40
+ const tier = Number(p.tier);
41
+ const expiresAt = Number(p.expiresAt);
42
+ if (!Number.isFinite(tier) || !Number.isFinite(expiresAt)) return null;
43
+ return {
44
+ tier,
45
+ grantedAt: Number(p.grantedAt) || null,
46
+ expiresAt,
47
+ nonce: typeof p.nonce === 'string' ? p.nonce : null,
48
+ version: Number(p.version) || SUPPORT_CONSENT_VERSION,
49
+ };
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Effective consent status, with the expiry applied. `enabled` is true only when
57
+ * a grant exists AND hasn't expired. `now` is injectable for tests.
58
+ */
59
+ export function consentStatus(globalDir, { now = Date.now } = {}) {
60
+ const rec = loadConsent(globalDir);
61
+ const t = now();
62
+ if (!rec) return { enabled: false, tier: 0, expiresAt: null, remainingMs: 0, file: consentFile(globalDir) };
63
+ const remainingMs = Math.max(0, rec.expiresAt - t);
64
+ const enabled = remainingMs > 0;
65
+ return {
66
+ enabled,
67
+ tier: enabled ? rec.tier : 0,
68
+ grantedAt: rec.grantedAt,
69
+ expiresAt: rec.expiresAt,
70
+ remainingMs,
71
+ file: consentFile(globalDir),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Grant support consent at `tier` for `minutes`. Overwrites any prior grant (a
77
+ * fresh, explicit gesture). Returns the stored record, or null if it couldn't be
78
+ * persisted. `minutes` is clamped to (0, MAX_GRANT_MINUTES]; `tier` to [1, MAX_TIER].
79
+ */
80
+ export function grantConsent(globalDir, { tier = 1, minutes = 60, now = Date.now } = {}) {
81
+ const clampedTier = Math.min(MAX_TIER, Math.max(1, Math.floor(Number(tier) || 1)));
82
+ const clampedMinutes = Math.min(MAX_GRANT_MINUTES, Math.max(1, Math.floor(Number(minutes) || 1)));
83
+ const grantedAt = now();
84
+ const rec = {
85
+ tier: clampedTier,
86
+ grantedAt,
87
+ expiresAt: grantedAt + clampedMinutes * 60_000,
88
+ nonce: crypto.randomBytes(12).toString('base64url'),
89
+ version: SUPPORT_CONSENT_VERSION,
90
+ };
91
+ try {
92
+ fs.mkdirSync(globalDir, { recursive: true });
93
+ fs.writeFileSync(consentFile(globalDir), JSON.stringify(rec, null, 2), { mode: 0o600 });
94
+ } catch {
95
+ return null;
96
+ }
97
+ return rec;
98
+ }
99
+
100
+ /** Revoke instantly by deleting the file. Returns true if a grant was removed. */
101
+ export function revokeConsent(globalDir) {
102
+ try {
103
+ fs.rmSync(consentFile(globalDir));
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ export function auditFile(globalDir) {
111
+ return path.join(globalDir, 'support-audit.jsonl');
112
+ }
113
+
114
+ /**
115
+ * The user-owned support audit: the most recent `limit` actions the daemon
116
+ * recorded (newest first). Written by the bmo-sync daemon (one JSON object per
117
+ * line) into the same machine-global dir; this is the read side the UI/CLI shows.
118
+ * Best-effort — a missing/corrupt file yields an empty list.
119
+ */
120
+ export function readAudit(globalDir, { limit = 50 } = {}) {
121
+ let raw;
122
+ try {
123
+ raw = fs.readFileSync(auditFile(globalDir), 'utf8');
124
+ } catch {
125
+ return [];
126
+ }
127
+ const lines = raw.split('\n').filter(Boolean);
128
+ const out = [];
129
+ for (const line of lines.slice(-limit)) {
130
+ try { out.push(JSON.parse(line)); } catch { /* skip a partial/corrupt line */ }
131
+ }
132
+ return out.reverse(); // newest first
133
+ }