create-workframe 0.1.12 → 0.1.13

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 (188) hide show
  1. package/README.md +1 -1
  2. package/bin/create-workframe.js +2 -2
  3. package/bin/workframe.js +130 -2
  4. package/docs/security.md +2 -2
  5. package/package.json +1 -1
  6. package/scripts/bundle-workframe-ui.mjs +9 -0
  7. package/scripts/sync-canonical-to-package.mjs +1 -0
  8. package/scripts/verify-public-deploy.sh +65 -3
  9. package/workframe-api/action_proxy.py +23 -17
  10. package/workframe-api/activity_feed.py +798 -0
  11. package/workframe-api/api_meta.py +157 -0
  12. package/workframe-api/auth_gate.py +566 -0
  13. package/workframe-api/avatar_registry.py +343 -0
  14. package/workframe-api/broker_audit.py +164 -0
  15. package/workframe-api/cell_authority.py +231 -0
  16. package/workframe-api/chat_bind.py +319 -0
  17. package/workframe-api/chat_sessions.py +509 -0
  18. package/workframe-api/chat_stream.py +694 -0
  19. package/workframe-api/cleanup_dogfood_smoke.py +252 -0
  20. package/workframe-api/credential_broker.py +162 -0
  21. package/workframe-api/credential_resolve.py +202 -0
  22. package/workframe-api/credential_store.py +245 -0
  23. package/workframe-api/crew_registry.py +250 -0
  24. package/workframe-api/db_schema.py +755 -0
  25. package/workframe-api/docker_gateway.py +166 -0
  26. package/workframe-api/doctor_runtime.py +109 -0
  27. package/workframe-api/domain/__init__.py +47 -0
  28. package/workframe-api/domain/entities.py +252 -0
  29. package/workframe-api/domain/schema/workframe-domain.schema.json +278 -0
  30. package/workframe-api/egress_policy.py +42 -0
  31. package/workframe-api/handler_modules/__init__.py +17 -0
  32. package/workframe-api/handler_modules/handler_admin.py +542 -0
  33. package/workframe-api/handler_modules/handler_auth.py +512 -0
  34. package/workframe-api/handler_modules/handler_chat.py +641 -0
  35. package/workframe-api/handler_modules/handler_install.py +178 -0
  36. package/workframe-api/handler_modules/handler_provider.py +325 -0
  37. package/workframe-api/handler_modules/handler_workspace.py +1420 -0
  38. package/workframe-api/health_monitor.py +80 -0
  39. package/workframe-api/hermes_admin.py +756 -0
  40. package/workframe-api/hermes_profiles.py +1328 -0
  41. package/workframe-api/install_api.py +123 -0
  42. package/workframe-api/kanban_cron.py +473 -0
  43. package/workframe-api/lane_bindings.py +423 -0
  44. package/workframe-api/llm_proxy.py +17 -43
  45. package/workframe-api/mention_helpers.py +180 -0
  46. package/workframe-api/mention_invoke.py +431 -0
  47. package/workframe-api/model_surface.py +1139 -0
  48. package/workframe-api/oauth_pending.py +85 -0
  49. package/workframe-api/oauth_redirect.py +381 -0
  50. package/workframe-api/package.json +2 -2
  51. package/workframe-api/profile_api_lifecycle.py +66 -0
  52. package/workframe-api/profile_gateway.py +436 -0
  53. package/workframe-api/provider_bindings.py +673 -0
  54. package/workframe-api/provider_bootstrap.py +347 -0
  55. package/workframe-api/provider_catalog.py +180 -0
  56. package/workframe-api/rooms.py +1613 -0
  57. package/workframe-api/route_registry.py +331 -191
  58. package/workframe-api/run-typecheck.mjs +2 -1
  59. package/workframe-api/run_authority.py +287 -0
  60. package/workframe-api/run_ledger.py +470 -0
  61. package/workframe-api/run_surface_wiring.py +344 -0
  62. package/workframe-api/runtime_cohort.py +752 -0
  63. package/workframe-api/runtime_tokens.py +127 -0
  64. package/workframe-api/server.py +1453 -19513
  65. package/workframe-api/snapshot_feed.py +49 -0
  66. package/workframe-api/stack_config.py +33 -1
  67. package/workframe-api/supervisor_client.py +154 -0
  68. package/workframe-api/test_api_meta_build_stamp.py +34 -0
  69. package/workframe-api/test_cell_authority.py +79 -0
  70. package/workframe-api/test_chat_bind.py +48 -0
  71. package/workframe-api/test_credential_lease_matrix.py +171 -0
  72. package/workframe-api/test_credential_resolve.py +51 -0
  73. package/workframe-api/test_credential_store.py +27 -0
  74. package/workframe-api/test_dogfood_flows.py +346 -0
  75. package/workframe-api/test_domain_entities.py +152 -0
  76. package/workframe-api/test_exception_hygiene.py +12 -0
  77. package/workframe-api/test_hermes_admin.py +72 -0
  78. package/workframe-api/test_install_wizard_resume.py +78 -0
  79. package/workframe-api/test_local_bootstrap.py +67 -0
  80. package/workframe-api/test_mention_helpers.py +35 -0
  81. package/workframe-api/test_mention_invoke.py +26 -0
  82. package/workframe-api/test_model_surface_consistency.py +1 -0
  83. package/workframe-api/test_oauth_pending.py +41 -0
  84. package/workframe-api/test_provider_bindings.py +52 -0
  85. package/workframe-api/test_provider_catalog.py +28 -0
  86. package/workframe-api/test_route_registry.py +171 -35
  87. package/workframe-api/test_run_authority.py +112 -0
  88. package/workframe-api/test_run_ledger.py +157 -0
  89. package/workframe-api/test_run_surface_wiring.py +130 -0
  90. package/workframe-api/test_runtime_cohort.py +85 -0
  91. package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
  92. package/workframe-api/test_server_reexports.py +99 -0
  93. package/workframe-api/test_stack_config_install_smtp.py +7 -0
  94. package/workframe-api/turn_credentials.py +28 -13
  95. package/workframe-api/turn_overlay.py +715 -0
  96. package/workframe-api/updates.py +16 -1
  97. package/workframe-api/user_prefs.py +387 -0
  98. package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
  99. package/workframe-api/workspace_bootstrap.py +536 -0
  100. package/workframe-api/workspace_files.py +344 -0
  101. package/workframe-api/workspace_messaging.py +206 -0
  102. package/workframe-api/zk_auth.py +3 -2
  103. package/workframe-supervisor/server.py +10 -1
  104. package/workframe-supervisor/test_supervisor_negative.py +75 -0
  105. package/workframe-ui/public/assets/{arc-B0OFRGmJ.js → arc-aUUffclm.js} +1 -1
  106. package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
  107. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DeBNltHS.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
  108. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BDhCHu7I.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
  109. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-olcDYvtI.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
  110. package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
  111. package/workframe-ui/public/assets/{chunk-2J33WTMH-D0obCBn_.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
  112. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DX93f-ZZ.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
  113. package/workframe-ui/public/assets/{chunk-4BX2VUAB-BX9B5wUs.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
  114. package/workframe-ui/public/assets/{chunk-55IACEB6-C4DGc6RO.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
  115. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-Crlja9Lf.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
  116. package/workframe-ui/public/assets/{chunk-727SXJPM-JYSm3szI.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
  117. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-_KslxVEl.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
  118. package/workframe-ui/public/assets/{chunk-BSJP7CBP-CpTO-jU8.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
  119. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-JGUkCmbI.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
  120. package/workframe-ui/public/assets/{chunk-FMBD7UC4-BscBwGaC.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-KSCS5N6A-5ZTZ0bpX.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
  122. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-DxvitYbW.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
  123. package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
  124. package/workframe-ui/public/assets/{chunk-ND2GUHAM-Cayl0iV9.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
  125. package/workframe-ui/public/assets/{chunk-NZK2D7GU-_7dyBR5G.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
  126. package/workframe-ui/public/assets/{chunk-O5CBEL6O--xTpD0yi.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
  127. package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
  128. package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
  129. package/workframe-ui/public/assets/{chunk-XPW4576I-CwxJQaeK.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
  130. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
  131. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
  132. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CCspENYv.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
  133. package/workframe-ui/public/assets/{dagre-BM42HDAG-Dv8V6R60.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
  134. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Da48hFPT.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
  135. package/workframe-ui/public/assets/{diagram-5GNKFQAL-dTihc7-3.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
  136. package/workframe-ui/public/assets/{diagram-KO2AKTUF-D_Jqu699.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
  137. package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt8PtEaZ.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
  138. package/workframe-ui/public/assets/{diagram-OG6HWLK6-BtmPjlh0.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
  139. package/workframe-ui/public/assets/{dist-Cz2turIT.js → dist-mQWmB3z6.js} +1 -1
  140. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-BxMGCflX.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
  141. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
  142. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-ivyroIZt.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
  143. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-B9llwShx.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
  144. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BFQHk4bw.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
  145. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-ursegLbc.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
  146. package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
  147. package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
  148. package/workframe-ui/public/assets/{info-J43DQDTF-DQpifAsB.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
  149. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-C16XP2Q7.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
  150. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-CiJoz7rP.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
  151. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CVwEvvUL.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
  152. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CWQ6hX5i.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
  153. package/workframe-ui/public/assets/{line-CgQsmU35.js → line-BTCEHb7l.js} +1 -1
  154. package/workframe-ui/public/assets/{linear-Bxbc77dL.js → linear-CkbydpnK.js} +1 -1
  155. package/workframe-ui/public/assets/{mermaid-parser.core-C_jMeF0a.js → mermaid-parser.core-DvrtArpk.js} +2 -2
  156. package/workframe-ui/public/assets/{mermaid.core-4RKV9MZP.js → mermaid.core-yj_1x_qj.js} +3 -3
  157. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-DZ7y7Sz0.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
  158. package/workframe-ui/public/assets/{packet-YPE3B663-B9h2I7Vq.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
  159. package/workframe-ui/public/assets/{pie-LRSECV5Y-B2mP5uHm.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
  160. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyYKyKiP.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
  161. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CL_6nej-.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
  162. package/workframe-ui/public/assets/{radar-GUYGQ44K-CNKNDQ7S.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
  163. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-eWU_mhFa.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
  164. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DrjcXvEQ.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
  165. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BHWR1xTJ.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
  166. package/workframe-ui/public/assets/{src-DfJB8kV1.js → src-D6mvlP96.js} +1 -1
  167. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BGJbSd3I.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
  168. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
  169. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DkwoCjiZ.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
  170. package/workframe-ui/public/assets/{treeView-BLDUP644-GxfhiqoW.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
  171. package/workframe-ui/public/assets/{treemap-LRROVOQU-DxrOsars.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
  172. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BsWhpUfr.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
  173. package/workframe-ui/public/assets/{wardley-L42UT6IY-CrFZWMHS.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
  174. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DV8Xpf5I.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
  175. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DqMqnwdZ.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
  176. package/workframe-ui/public/index.html +7 -5
  177. package/workframe-ui/public/workframe-build.json +5 -0
  178. package/workframe-ui/public/assets/architecture-7EHR7CIX-Dwwi9yA0.js +0 -1
  179. package/workframe-ui/public/assets/channel-fNHbDpV4.js +0 -1
  180. package/workframe-ui/public/assets/chunk-LZXEDZCA-DMfdMUwz.js +0 -2
  181. package/workframe-ui/public/assets/chunk-QZHKN3VN-ewTgEQK8.js +0 -1
  182. package/workframe-ui/public/assets/chunk-WU5MYG2G-D4Tg-A0l.js +0 -1
  183. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CFAgBpEl.js +0 -1
  184. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CFAgBpEl.js +0 -1
  185. package/workframe-ui/public/assets/eventmodeling-FCH6USID-BIJTP5k-.js +0 -1
  186. package/workframe-ui/public/assets/index-8CuZDEIG.css +0 -1
  187. package/workframe-ui/public/assets/index-xS9lFekI.js +0 -129
  188. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CMLuNyeC.js +0 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Published on npm as **create-workframe**.
4
4
 
5
5
  ```bash
6
- npx create-workframe@0.1.12 MyProject
6
+ npx create-workframe@0.1.13 MyProject
7
7
  ```
8
8
 
9
9
  Scaffolds an isolated Workframe + Hermes project on Windows, macOS, and Linux.
@@ -535,7 +535,7 @@ services:
535
535
  context: \${WORKFRAME_HOST_PROJECT_ROOT}/workframe-supervisor
536
536
  volumes:
537
537
  - \${WORKFRAME_HOST_PROJECT_ROOT}/Agents:/opt/data
538
- - \${WORKFRAME_HOST_PROJECT_ROOT}:\${WORKFRAME_HOST_PROJECT_ROOT}
538
+ - \${WORKFRAME_HOST_PROJECT_ROOT}:/compose
539
539
 
540
540
  workframe:
541
541
  volumes:
@@ -2527,7 +2527,7 @@ async function main() {
2527
2527
  copyWorkframeUiTemplate(target, [nativeSlug]);
2528
2528
  writeText(
2529
2529
  path.join(target, 'workframe-ui', 'public', 'workframe-config.json'),
2530
- `${JSON.stringify({ project_name: safeName, native_profile: nativeSlug }, null, 2)}\n`,
2530
+ `${JSON.stringify({ project_name: safeName, native_profile: nativeSlug, package_version: PKG_VERSION }, null, 2)}\n`,
2531
2531
  );
2532
2532
 
2533
2533
  writeText(path.join(target, 'SETUP.md'), onboardingDoc({
package/bin/workframe.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * workframe — lifecycle CLI for generated Workframe projects.
4
4
  *
5
5
  * Commands:
6
- * workframe doctor [--repair] Diagnose stack; --repair provisions missing agent DM runtimes
6
+ * workframe doctor [--repair] [--json] Diagnose stack; --json mutation-free report
7
7
  * workframe setup Open Hermes setup (credentials)
8
8
  * workframe stop Stop all stack containers
9
9
  * workframe start Start the full stack (docker compose up -d)
@@ -53,9 +53,137 @@ function doctorAgentDmRuntimes(root, { repair = false } = {}) {
53
53
  return dockerCompose(root, ['exec', '-T', 'workframe-api', 'python3', '-c', py], { stdio: 'pipe' });
54
54
  }
55
55
 
56
+ function redactPath(root, absolutePath) {
57
+ try {
58
+ const rel = path.relative(root, absolutePath);
59
+ if (!rel || rel.startsWith('..')) return path.basename(absolutePath);
60
+ return rel.split(path.sep).join('/');
61
+ } catch {
62
+ return path.basename(String(absolutePath || ''));
63
+ }
64
+ }
65
+
66
+ function nodeVersion() {
67
+ const res = spawnSync(process.execPath, ['-v'], { encoding: 'utf8' });
68
+ if (res.status !== 0) return null;
69
+ return (res.stdout || res.stderr || '').trim() || null;
70
+ }
71
+
72
+ function buildDoctorJsonReport(root, manifest, issues, checks) {
73
+ const decision = issues.length === 0
74
+ ? 'healthy'
75
+ : checks.docker?.status === 'fail'
76
+ ? 'needs_user_action'
77
+ : 'unhealthy';
78
+ return {
79
+ schema_version: '0.1',
80
+ advisory_only: true,
81
+ decision,
82
+ project: {
83
+ name: manifest?.project_name || path.basename(root),
84
+ package_version: manifest?.package_version || null,
85
+ },
86
+ checks,
87
+ issues,
88
+ };
89
+ }
90
+
91
+ function collectDoctorChecks(root, manifest) {
92
+ const checks = {};
93
+ const issues = [];
94
+
95
+ const dockerCheck = spawnSync('docker', ['info'], { encoding: 'utf8' });
96
+ if (dockerCheck.status !== 0) {
97
+ checks.docker = { status: 'fail', note: 'Docker not running or not installed' };
98
+ issues.push('Docker is not running or not installed.');
99
+ } else {
100
+ checks.docker = { status: 'ok', note: 'docker info succeeded' };
101
+ }
102
+
103
+ const nodeVer = nodeVersion();
104
+ checks.node = {
105
+ status: nodeVer ? 'ok' : 'skip',
106
+ version: nodeVer,
107
+ };
108
+
109
+ if (!manifest) {
110
+ checks.manifest = { status: 'fail', stack: null };
111
+ issues.push('No workframe-manifest.json found.');
112
+ } else {
113
+ checks.manifest = {
114
+ status: 'ok',
115
+ stack: manifest.docker?.stack || null,
116
+ };
117
+ }
118
+
119
+ const layout = { workspace: 'missing', runtime: 'missing' };
120
+ for (const dir of ['Files', 'Agents']) {
121
+ if (fs.existsSync(path.join(root, dir))) {
122
+ layout[dir === 'Files' ? 'workspace' : 'runtime'] = 'ok';
123
+ } else {
124
+ issues.push(`${dir}/ directory missing.`);
125
+ }
126
+ }
127
+ checks.layout = { status: layout.workspace === 'ok' && layout.runtime === 'ok' ? 'ok' : 'fail', ...layout };
128
+
129
+ checks.cell = {
130
+ deployment_mode: manifest?.security?.deployment_mode || manifest?.deployment_mode || null,
131
+ install_id_redacted: manifest?.install_id ? String(manifest.install_id).slice(0, 8) + '…' : null,
132
+ manifest_path: redactPath(root, path.join(root, 'workframe-manifest.json')),
133
+ };
134
+
135
+ const runtimeCandidates = [];
136
+ if (manifest && checks.docker?.status === 'ok') {
137
+ const ps = dockerCompose(root, ['ps', '--format', 'json'], { stdio: 'pipe' });
138
+ if (ps.status === 0 && ps.stdout) {
139
+ try {
140
+ const raw = ps.stdout.trim();
141
+ const containers = raw.startsWith('[')
142
+ ? JSON.parse(raw)
143
+ : raw.split('\n').filter(Boolean).map((line) => JSON.parse(line));
144
+ for (const name of ['gateway', 'workframe-api', 'workframe']) {
145
+ const container = containers.find((c) => c.Name?.includes(name) || c.Service === name);
146
+ runtimeCandidates.push({
147
+ kind: name === 'gateway' ? 'hermes_managed' : 'workframe_service',
148
+ service: name,
149
+ status: container ? (container.State === 'running' ? 'running' : container.State || 'unknown') : 'stopped',
150
+ });
151
+ if (!container || container.State !== 'running') {
152
+ issues.push(`${name} container not running.`);
153
+ }
154
+ }
155
+ } catch {
156
+ runtimeCandidates.push({ kind: 'hermes_managed', service: 'gateway', status: 'unknown' });
157
+ }
158
+ }
159
+ } else {
160
+ runtimeCandidates.push({ kind: 'hermes_managed', service: 'gateway', status: 'unknown', note: 'docker unavailable' });
161
+ }
162
+ checks.runtime_candidates = runtimeCandidates;
163
+
164
+ if (!fs.existsSync(path.join(root, '.env'))) {
165
+ issues.push('.env file missing.');
166
+ }
167
+ if (!fs.existsSync(path.join(root, 'docker-compose.yml'))) {
168
+ issues.push('docker-compose.yml missing.');
169
+ }
170
+
171
+ return { checks, issues };
172
+ }
173
+
56
174
  function cmdDoctor(root, extraArgs = []) {
57
175
  const repair = extraArgs.includes('--repair');
176
+ const jsonMode = extraArgs.includes('--json');
58
177
  const manifest = readManifest(root);
178
+
179
+ if (jsonMode) {
180
+ const { checks, issues } = collectDoctorChecks(root, manifest);
181
+ const report = buildDoctorJsonReport(root, manifest, issues, checks);
182
+ console.log(JSON.stringify(report, null, 2));
183
+ process.exit(report.decision === 'healthy' ? 0 : 1);
184
+ return;
185
+ }
186
+
59
187
  const issues = [];
60
188
 
61
189
  console.log(repair ? 'workframe doctor --repair' : 'workframe doctor');
@@ -287,7 +415,7 @@ function usage() {
287
415
  console.log(`workframe — lifecycle CLI for Workframe projects
288
416
 
289
417
  Usage:
290
- workframe doctor [--repair] Diagnose stack; --repair provisions missing agent DM runtimes
418
+ workframe doctor [--repair] [--json] Diagnose stack; --json mutation-free report
291
419
  workframe setup Open Hermes setup (credentials)
292
420
  workframe start Start the full stack (docker compose up -d)
293
421
  workframe stop Stop all stack containers
package/docs/security.md CHANGED
@@ -53,7 +53,7 @@ In `public_multi_user`, credential-mediated traffic must not depend on agent coo
53
53
  2. Runtime profiles use **lease tokens** (`wf_rt_*`) and internal proxy base URLs for LLM calls.
54
54
  3. Integrated action providers use `/internal/action/*` with the same lease model.
55
55
  4. Gateway must **not** join the supervisor `control-net` (already enforced in compose).
56
- 5. Optional strict profile (`WORKFRAME_FORCE_AGENT_EGRESS_BROKER=true`) may block **direct** outbound connections to known provider API hostnames so traffic must use the broker — general internet stays open.
56
+ 5. Optional strict profile (`WORKFRAME_FORCE_AGENT_EGRESS_BROKER=true`) blocks **direct** outbound connections to known provider API hostnames via the `gateway-egress-guard` iptables sidecar (`docker-compose.egress-broker.yml`) so traffic must use the broker — general internet stays open.
57
57
 
58
58
  This is **routing and secret placement**, not disabling `terminal` or adding human approval before each tool call. Lease validation on brokered requests is in-process (milliseconds), not an operator gate.
59
59
 
@@ -63,7 +63,7 @@ This is **routing and secret placement**, not disabling `terminal` or adding hum
63
63
  |------|----------------|----------------------|
64
64
  | `trusted_team` / dogfood | Unrestricted | Config + vault + leases (today) |
65
65
  | `public_multi_user` | Unrestricted (agents stay capable) | Same broker path; verify script reports posture |
66
- | `public_multi_user` + `WORKFRAME_FORCE_AGENT_EGRESS_BROKER=true` | Unrestricted except provider-host deny/redirect (planned) | Network-enforced broker path |
66
+ | `public_multi_user` + `WORKFRAME_FORCE_AGENT_EGRESS_BROKER=true` | Unrestricted except provider-host deny via egress-guard sidecar | Network-enforced broker path |
67
67
 
68
68
  See also [Audit 0027 — Agent Vault comparison](../audits/0027-agent-vault-comparison.md) (advisory; Infisical Agent Vault as a reference pattern, not a required dependency).
69
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-workframe",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Scaffold a Workframe + Hermes workspace with guided onboarding",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,6 +59,15 @@ if (!fs.existsSync(path.join(dist, 'index.html'))) {
59
59
  console.log(`Copying ${dist} -> ${UI_DEST}`);
60
60
  copyTree(dist, UI_DEST);
61
61
 
62
+ const pkgVersion = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version;
63
+ const gitRef = spawnSync('git', ['rev-parse', '--short', 'HEAD'], { encoding: 'utf8', cwd: REPO_ROOT });
64
+ const buildStamp = {
65
+ package_version: pkgVersion,
66
+ bundled_at: new Date().toISOString(),
67
+ git_ref: gitRef.status === 0 ? gitRef.stdout.trim() : '',
68
+ };
69
+ fs.writeFileSync(path.join(UI_DEST, 'workframe-build.json'), `${JSON.stringify(buildStamp, null, 2)}\n`);
70
+
62
71
  const avatarShared = path.join(PKG_ROOT, 'shared', 'agent-avatars');
63
72
 
64
73
  function copyPresetPngs(src, dst, { keepCatalog = false } = {}) {
@@ -110,6 +110,7 @@ const applyScripts = [
110
110
  'setup-stack-secrets.sh',
111
111
  'bootstrap-workspace-link.sh',
112
112
  'verify-public-deploy.sh',
113
+ 'gateway-egress-iptables.sh',
113
114
  'fix-zk-encryption-key.sh',
114
115
  'set-compose-public-url.mjs',
115
116
  'ensure-compose-host-paths.mjs',
@@ -20,9 +20,10 @@ gateway_container_name() {
20
20
  }
21
21
 
22
22
  report_egress_posture() {
23
- local gw force_broker gw_on_control internal_control
23
+ local gw force_broker gw_on_control internal_control egress_overlay
24
24
  gw="$(gateway_container_name)"
25
25
  force_broker="$(env_val WORKFRAME_FORCE_AGENT_EGRESS_BROKER)"
26
+ egress_overlay="$COMPOSE_DIR/docker-compose.egress-broker.yml"
26
27
 
27
28
  if grep -A30 '^ gateway:' "$COMPOSE_FILE" | grep -q 'control-net'; then
28
29
  gw_on_control=yes
@@ -48,7 +49,15 @@ report_egress_posture() {
48
49
  if echo "$nets" | grep -qi 'control'; then
49
50
  fail "running gateway is attached to a control network"
50
51
  fi
51
- info "egress gateway_egress_posture=unrestricted_general (broker for LLM/action providers)"
52
+ if [[ "$force_broker" =~ ^(1|true|yes|on)$ ]]; then
53
+ if docker inspect workframe-gateway-egress-guard >/dev/null 2>&1; then
54
+ info "egress gateway_egress_posture=provider_hosts_blocked (egress-guard sidecar)"
55
+ else
56
+ fail "WORKFRAME_FORCE_AGENT_EGRESS_BROKER=true but gateway-egress-guard is not running"
57
+ fi
58
+ else
59
+ info "egress gateway_egress_posture=unrestricted_general (broker for LLM/action providers)"
60
+ fi
52
61
  else
53
62
  info "egress gateway_egress_posture=unknown (gateway container not running)"
54
63
  fi
@@ -58,11 +67,60 @@ report_egress_posture() {
58
67
  info "egress broker_latency=per-request lease validation (no human approval gate)"
59
68
 
60
69
  if [[ "$force_broker" =~ ^(1|true|yes|on)$ ]]; then
61
- fail "WORKFRAME_FORCE_AGENT_EGRESS_BROKER=true is set but provider-host egress enforcement is not implemented yet"
70
+ [[ -f "$egress_overlay" ]] || fail "missing $egress_overlay (required when WORKFRAME_FORCE_AGENT_EGRESS_BROKER=true)"
71
+ grep -q 'gateway-egress-guard' "$egress_overlay" || fail "egress overlay missing gateway-egress-guard service"
72
+ ok "forced broker egress overlay present"
62
73
  fi
63
74
  ok "egress posture reported"
64
75
  }
65
76
 
77
+ report_supervisor_constraints() {
78
+ local public_overlay="$COMPOSE_DIR/docker-compose.public.yml"
79
+ if [[ -f "$public_overlay" ]]; then
80
+ grep -q 'WORKFRAME_SUPERVISOR_ALLOW_RAW_EXEC=0' "$public_overlay" \
81
+ || fail "docker-compose.public.yml must set WORKFRAME_SUPERVISOR_ALLOW_RAW_EXEC=0"
82
+ if grep -A30 '^ workframe-api:' "$public_overlay" | grep -q '/var/run/docker.sock'; then
83
+ fail "docker-compose.public.yml must not mount docker.sock on workframe-api"
84
+ fi
85
+ ok "supervisor public overlay constraints"
86
+ else
87
+ warn "docker-compose.public.yml missing — skipping supervisor overlay checks"
88
+ fi
89
+ }
90
+
91
+ report_broker_audit_schema() {
92
+ local data_dir
93
+ data_dir="$(cd "$ROOT" && pwd)/runtime/workframe-api-data"
94
+ if [[ ! -d "$data_dir" ]]; then
95
+ info "broker_audit schema=skipped (no runtime/workframe-api-data)"
96
+ return
97
+ fi
98
+ python3 - "$data_dir" <<'PY' || fail "broker_audit_events table missing (run API once or apply migration)"
99
+ import sqlite3, sys
100
+ from pathlib import Path
101
+ db = Path(sys.argv[1]) / "workframe.db"
102
+ if not db.is_file():
103
+ print("skip: no workframe.db yet")
104
+ raise SystemExit(0)
105
+ conn = sqlite3.connect(str(db))
106
+ row = conn.execute(
107
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='broker_audit_events'"
108
+ ).fetchone()
109
+ conn.close()
110
+ if not row:
111
+ raise SystemExit(1)
112
+ PY
113
+ ok "broker audit schema"
114
+ }
115
+
116
+ report_backup_restore_evidence() {
117
+ local backup_doc="$ROOT/infra/compose/workframe/PUBLIC_DEPLOY.md"
118
+ [[ -f "$backup_doc" ]] || fail "missing PUBLIC_DEPLOY.md backup section"
119
+ grep -qi 'backup' "$backup_doc" || fail "PUBLIC_DEPLOY.md missing backup guidance"
120
+ grep -qi 'rollback\|restore' "$backup_doc" || fail "PUBLIC_DEPLOY.md missing restore/rollback guidance"
121
+ ok "backup/restore documented"
122
+ }
123
+
66
124
  [[ -f "$ENV_FILE" ]] || fail "missing $ENV_FILE"
67
125
  [[ -f "$COMPOSE_FILE" ]] || fail "missing $COMPOSE_FILE"
68
126
 
@@ -158,4 +216,8 @@ fi
158
216
 
159
217
  report_egress_posture
160
218
 
219
+ report_supervisor_constraints
220
+ report_broker_audit_schema
221
+ report_backup_restore_evidence
222
+
161
223
  ok "public_multi_user preflight passed"
@@ -9,8 +9,8 @@ import urllib.request
9
9
  from http.server import BaseHTTPRequestHandler
10
10
  from typing import Any, Callable
11
11
 
12
- import llm_proxy
13
- import turn_credentials
12
+ import credential_broker
13
+ import internal_proxy_auth
14
14
 
15
15
  UPSTREAM_BASE: dict[str, str] = {
16
16
  "github": "https://api.github.com",
@@ -21,6 +21,15 @@ UPSTREAM_BASE: dict[str, str] = {
21
21
  PROXY_PATH_RE = re.compile(r"^/internal/action/([a-z0-9_-]+)(/.*)?$", re.IGNORECASE)
22
22
 
23
23
 
24
+ def _upstream_host(provider: str) -> str:
25
+ base = UPSTREAM_BASE.get(str(provider or "").strip().lower(), "")
26
+ return str(base).split("://", 1)[-1].split("/", 1)[0].lower()
27
+
28
+
29
+ def authorize_internal_proxy(handler: BaseHTTPRequestHandler) -> tuple[bool, str]:
30
+ return internal_proxy_auth.authorize_internal_proxy(handler)
31
+
32
+
24
33
  def upstream_auth_header(provider: str, secret: str) -> dict[str, str]:
25
34
  provider = str(provider or "").strip().lower()
26
35
  secret = str(secret or "").strip()
@@ -45,24 +54,21 @@ def forward_request(
45
54
  if not base:
46
55
  return 404, {"Content-Type": "application/json"}, json.dumps({"error": "unknown provider"}).encode()
47
56
 
48
- token = llm_proxy.extract_bearer(headers)
49
- lease = turn_credentials.validate_lease(token)
50
- if not lease:
51
- return 401, {"Content-Type": "application/json"}, json.dumps({"error": "invalid lease"}).encode()
52
- if str(lease.get("provider") or "").lower() != provider:
53
- return 403, {"Content-Type": "application/json"}, json.dumps({"error": "provider mismatch"}).encode()
54
-
55
- ok_profile, profile_err, profile_status = llm_proxy.validate_lease_profile(lease, headers)
56
- if not ok_profile:
57
+ auth = credential_broker.authorize_broker_lease(
58
+ provider,
59
+ headers,
60
+ resolve_secret=resolve_secret,
61
+ broker_kind="action",
62
+ upstream_host=_upstream_host(provider),
63
+ )
64
+ if not auth.ok:
57
65
  return (
58
- profile_status,
66
+ auth.status,
59
67
  {"Content-Type": "application/json"},
60
- json.dumps({"error": profile_err}).encode(),
68
+ credential_broker.broker_error_body(auth),
61
69
  )
62
70
 
63
- _env_var, secret = turn_credentials.resolve_lease_secret(lease, resolve_secret)
64
- if not secret:
65
- return 402, {"Content-Type": "application/json"}, json.dumps({"error": "no credential"}).encode()
71
+ secret = auth.secret
66
72
 
67
73
  path = subpath if subpath.startswith("/") else f"/{subpath}"
68
74
  url = f"{base.rstrip('/')}{path}"
@@ -100,7 +106,7 @@ def handle_proxy_request(
100
106
  *,
101
107
  resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
102
108
  ) -> bool:
103
- ok, err = llm_proxy.authorize_internal_proxy(handler)
109
+ ok, err = authorize_internal_proxy(handler)
104
110
  if not ok:
105
111
  handler.send_response(403)
106
112
  handler.send_header("Content-Type", "application/json")