create-workframe 0.1.11 → 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 (195) 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 +85 -0
  5. package/package.json +1 -1
  6. package/scripts/bundle-workframe-ui.mjs +9 -0
  7. package/scripts/sync-canonical-to-package.mjs +9 -0
  8. package/scripts/verify-public-deploy.sh +121 -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/openrouter_catalog.py +4 -4
  51. package/workframe-api/package.json +2 -2
  52. package/workframe-api/profile_api_lifecycle.py +66 -0
  53. package/workframe-api/profile_config_yaml.py +126 -0
  54. package/workframe-api/profile_gateway.py +436 -0
  55. package/workframe-api/provider_bindings.py +673 -0
  56. package/workframe-api/provider_bootstrap.py +347 -0
  57. package/workframe-api/provider_catalog.py +180 -0
  58. package/workframe-api/rooms.py +1613 -0
  59. package/workframe-api/route_registry.py +470 -0
  60. package/workframe-api/run-typecheck.mjs +26 -0
  61. package/workframe-api/run_authority.py +287 -0
  62. package/workframe-api/run_ledger.py +470 -0
  63. package/workframe-api/run_surface_wiring.py +344 -0
  64. package/workframe-api/runtime_cohort.py +752 -0
  65. package/workframe-api/runtime_tokens.py +127 -0
  66. package/workframe-api/server.py +1376 -19305
  67. package/workframe-api/snapshot_feed.py +49 -0
  68. package/workframe-api/stack_config.py +33 -1
  69. package/workframe-api/supervisor_client.py +154 -0
  70. package/workframe-api/test_api_meta_build_stamp.py +34 -0
  71. package/workframe-api/test_cell_authority.py +79 -0
  72. package/workframe-api/test_chat_bind.py +48 -0
  73. package/workframe-api/test_credential_lease_matrix.py +171 -0
  74. package/workframe-api/test_credential_resolve.py +51 -0
  75. package/workframe-api/test_credential_store.py +27 -0
  76. package/workframe-api/test_dogfood_flows.py +346 -0
  77. package/workframe-api/test_domain_entities.py +152 -0
  78. package/workframe-api/test_exception_hygiene.py +12 -0
  79. package/workframe-api/test_hermes_admin.py +72 -0
  80. package/workframe-api/test_install_wizard_resume.py +78 -0
  81. package/workframe-api/test_local_bootstrap.py +67 -0
  82. package/workframe-api/test_mention_helpers.py +35 -0
  83. package/workframe-api/test_mention_invoke.py +26 -0
  84. package/workframe-api/test_model_surface_consistency.py +117 -0
  85. package/workframe-api/test_oauth_pending.py +41 -0
  86. package/workframe-api/test_profile_model_yaml.py +99 -0
  87. package/workframe-api/test_provider_bindings.py +52 -0
  88. package/workframe-api/test_provider_catalog.py +28 -0
  89. package/workframe-api/test_public_routes.py +41 -0
  90. package/workframe-api/test_route_registry.py +245 -0
  91. package/workframe-api/test_run_authority.py +112 -0
  92. package/workframe-api/test_run_ledger.py +157 -0
  93. package/workframe-api/test_run_surface_wiring.py +130 -0
  94. package/workframe-api/test_runtime_cohort.py +85 -0
  95. package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
  96. package/workframe-api/test_server_reexports.py +99 -0
  97. package/workframe-api/test_stack_config_install_smtp.py +7 -0
  98. package/workframe-api/time-bind-steps.py +75 -0
  99. package/workframe-api/time-perf.py +81 -0
  100. package/workframe-api/time-warm.py +107 -0
  101. package/workframe-api/turn_credentials.py +28 -13
  102. package/workframe-api/turn_overlay.py +715 -0
  103. package/workframe-api/updates.py +16 -1
  104. package/workframe-api/user_prefs.py +387 -0
  105. package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
  106. package/workframe-api/workspace_bootstrap.py +536 -0
  107. package/workframe-api/workspace_files.py +344 -0
  108. package/workframe-api/workspace_messaging.py +206 -0
  109. package/workframe-api/zk_auth.py +3 -2
  110. package/workframe-supervisor/server.py +10 -1
  111. package/workframe-supervisor/test_supervisor_negative.py +75 -0
  112. package/workframe-ui/public/assets/{arc-DjpS9a2b.js → arc-aUUffclm.js} +1 -1
  113. package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
  114. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
  115. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
  116. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
  117. package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
  118. package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
  119. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
  120. package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
  122. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
  123. package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
  124. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
  125. package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
  126. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
  127. package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
  128. package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
  129. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
  130. package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
  131. package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
  132. package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
  133. package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
  134. package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
  135. package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
  136. package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
  137. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
  138. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
  139. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
  140. package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
  141. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
  142. package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
  143. package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
  144. package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
  145. package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
  146. package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-mQWmB3z6.js} +1 -1
  147. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
  148. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
  149. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
  150. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
  151. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
  152. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
  153. package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
  154. package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
  155. package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
  156. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
  157. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
  158. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
  159. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
  160. package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-BTCEHb7l.js} +1 -1
  161. package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-CkbydpnK.js} +1 -1
  162. package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-DvrtArpk.js} +2 -2
  163. package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-yj_1x_qj.js} +3 -3
  164. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
  165. package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
  166. package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
  167. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
  168. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
  169. package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
  170. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
  171. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
  172. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
  173. package/workframe-ui/public/assets/{src-CY_By-r7.js → src-D6mvlP96.js} +1 -1
  174. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
  175. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
  176. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
  177. package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
  178. package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
  179. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
  180. package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
  181. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
  182. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
  183. package/workframe-ui/public/index.html +7 -6
  184. package/workframe-ui/public/workframe-build.json +5 -0
  185. package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
  186. package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
  187. package/workframe-ui/public/assets/chunk-LZXEDZCA-CsRdBqUB.js +0 -2
  188. package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
  189. package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
  190. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
  191. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
  192. package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
  193. package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
  194. package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
  195. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.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.9 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
@@ -0,0 +1,85 @@
1
+ # Security
2
+
3
+ Report vulnerabilities via [SECURITY.md](../../SECURITY.md) — not public GitHub issues.
4
+
5
+ ## Modes
6
+
7
+ | Variable | Effect |
8
+ |----------|--------|
9
+ | `SECURE_MODE=true` | Auth on mutating routes, restricted CORS, no Docker socket on API |
10
+ | `DEV_LOCAL_UNSAFE=true` | Development only — open CORS, relaxed auth |
11
+ | *(neither set)* | Secure default |
12
+
13
+ Never enable `DEV_LOCAL_UNSAFE` or `WORKFRAME_E2E=1` on a public HTTPS URL during the install window.
14
+
15
+ ## Supervisor boundary
16
+
17
+ With `SECURE_MODE=true`:
18
+
19
+ - Only `workframe-supervisor` mounts `/var/run/docker.sock`
20
+ - API calls supervisor with `WORKFRAME_SUPERVISOR_TOKEN`
21
+ - Public compose overlay removes the socket from `workframe-api`
22
+
23
+ ## Credentials
24
+
25
+ - **BYOK default** — user keys in per-user Hermes homes
26
+ - **`user_only` providers** — no workspace fallback
27
+ - **Vault** — envelope encryption; not mounted on the gateway container
28
+ - **Per-turn leases** — gateway proxy tokens instead of shared raw secrets
29
+
30
+ ## Agent egress and the credential broker
31
+
32
+ Hermes agents need the open internet — web research, docs, public APIs, and normal tool use must stay fast and unrestricted. Security work is **not** about blocking that traffic.
33
+
34
+ Workframe splits outbound traffic into two classes:
35
+
36
+ | Class | Examples | Policy |
37
+ |-------|----------|--------|
38
+ | **General egress** | Web search, `fetch`, public HTTPS, package indexes | **Allowed** — direct from the gateway container; no broker hop, no per-action approval |
39
+ | **Brokered egress** | LLM providers, GitHub/Vercel/Netlify when using vault credentials | **Required** — `wf_rt_*` lease + internal proxy; raw secrets stay in the API vault |
40
+
41
+ ```text
42
+ Hermes (gateway)
43
+ ├─ general egress ──────────► internet (research, tools, public APIs)
44
+ └─ brokered egress ─────────► workframe-api (/internal/llm/*, /internal/action/*)
45
+ └─ vault + lease validation ─► upstream provider
46
+ ```
47
+
48
+ ### What “unavoidable broker” means
49
+
50
+ In `public_multi_user`, credential-mediated traffic must not depend on agent cooperation alone. Concretely:
51
+
52
+ 1. Gateway must **not** receive raw provider API keys or the vault database.
53
+ 2. Runtime profiles use **lease tokens** (`wf_rt_*`) and internal proxy base URLs for LLM calls.
54
+ 3. Integrated action providers use `/internal/action/*` with the same lease model.
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`) 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
+
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
+
60
+ ### Deployment posture
61
+
62
+ | Mode | General egress | Brokered credentials |
63
+ |------|----------------|----------------------|
64
+ | `trusted_team` / dogfood | Unrestricted | Config + vault + leases (today) |
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 via egress-guard sidecar | Network-enforced broker path |
67
+
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
+
70
+ ## Public multi-user (`public_multi_user`)
71
+
72
+ | Control | Behavior |
73
+ |---------|----------|
74
+ | Boot | Requires HTTPS, vault KEK, SMTP, proxy token, supervisor token |
75
+ | Auth | Invite-only after install |
76
+ | Dashboard | Owner/admin only (`auth_request`) |
77
+ | Runtime RBAC | Members limited to their own `u-{user}-*` profiles |
78
+ | Gateway env | Allowlisted secrets only |
79
+ | Supervisor | Blocks direct access to user `.env` and `auth.json` paths |
80
+
81
+ Full checklist: [PUBLIC_DEPLOY.md](../../infra/compose/workframe/PUBLIC_DEPLOY.md).
82
+
83
+ ## Multi-user filesystem model
84
+
85
+ The Hermes gateway container mounts the shared `Agents/` tree. User isolation uses per-user runtime profiles, role checks in the API, supervisor guards, and encrypted credentials. Read [PUBLIC_DEPLOY.md](../../infra/compose/workframe/PUBLIC_DEPLOY.md) before running a public multi-user install.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-workframe",
3
- "version": "0.1.11",
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',
@@ -137,6 +138,14 @@ if (fs.existsSync(publicDeploySrc)) {
137
138
  console.log('Synced PUBLIC_DEPLOY.md -> package/docs/');
138
139
  }
139
140
 
141
+ const securityPublicSrc = path.join(REPO_ROOT, 'docs/public/security.md');
142
+ const securityPkgDst = path.join(PKG_ROOT, 'docs/security.md');
143
+ if (fs.existsSync(securityPublicSrc)) {
144
+ fs.mkdirSync(path.dirname(securityPkgDst), { recursive: true });
145
+ fs.copyFileSync(securityPublicSrc, securityPkgDst);
146
+ console.log('Synced docs/public/security.md -> package/docs/security.md');
147
+ }
148
+
140
149
  for (const name of ['LICENSE', 'NOTICE', 'SECURITY.md']) {
141
150
  const src = path.join(REPO_ROOT, name);
142
151
  const dst = path.join(PKG_ROOT, name);
@@ -11,6 +11,115 @@ COMPOSE_FILE="$COMPOSE_DIR/docker-compose.yml"
11
11
  fail() { echo "FAIL: $*" >&2; exit 1; }
12
12
  warn() { echo "WARN: $*" >&2; }
13
13
  ok() { echo "OK: $*"; }
14
+ info() { echo "INFO: $*"; }
15
+
16
+ gateway_container_name() {
17
+ local name
18
+ name="$(grep -A20 '^ gateway:' "$COMPOSE_FILE" | grep 'container_name:' | head -n1 | sed 's/.*container_name:[[:space:]]*//' | tr -d '\r"')"
19
+ printf '%s' "${name:-workframe-gateway}"
20
+ }
21
+
22
+ report_egress_posture() {
23
+ local gw force_broker gw_on_control internal_control egress_overlay
24
+ gw="$(gateway_container_name)"
25
+ force_broker="$(env_val WORKFRAME_FORCE_AGENT_EGRESS_BROKER)"
26
+ egress_overlay="$COMPOSE_DIR/docker-compose.egress-broker.yml"
27
+
28
+ if grep -A30 '^ gateway:' "$COMPOSE_FILE" | grep -q 'control-net'; then
29
+ gw_on_control=yes
30
+ else
31
+ gw_on_control=no
32
+ fi
33
+ if grep -A5 'control-net:' "$COMPOSE_FILE" | grep -q 'internal:[[:space:]]*true'; then
34
+ internal_control=yes
35
+ else
36
+ internal_control=no
37
+ fi
38
+
39
+ info "egress gateway_container=$gw"
40
+ info "egress gateway_on_control_net=$gw_on_control (expected no)"
41
+ info "egress control_net_internal=$internal_control (expected yes)"
42
+ [[ "$gw_on_control" == "no" ]] || fail "gateway must not join control-net"
43
+ [[ "$internal_control" == "yes" ]] || warn "control-net is not marked internal:true"
44
+
45
+ if command -v docker >/dev/null 2>&1 && docker inspect "$gw" >/dev/null 2>&1; then
46
+ local nets
47
+ nets="$(docker inspect "$gw" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' 2>/dev/null || true)"
48
+ info "egress gateway_networks=${nets:-<unknown>}"
49
+ if echo "$nets" | grep -qi 'control'; then
50
+ fail "running gateway is attached to a control network"
51
+ fi
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
61
+ else
62
+ info "egress gateway_egress_posture=unknown (gateway container not running)"
63
+ fi
64
+
65
+ info "egress broker_path=config_plus_leases (/internal/llm/*, /internal/action/*)"
66
+ info "egress general_internet=allowed (research, terminal, public APIs)"
67
+ info "egress broker_latency=per-request lease validation (no human approval gate)"
68
+
69
+ if [[ "$force_broker" =~ ^(1|true|yes|on)$ ]]; then
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"
73
+ fi
74
+ ok "egress posture reported"
75
+ }
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
+ }
14
123
 
15
124
  [[ -f "$ENV_FILE" ]] || fail "missing $ENV_FILE"
16
125
  [[ -f "$COMPOSE_FILE" ]] || fail "missing $COMPOSE_FILE"
@@ -85,6 +194,9 @@ if command -v curl >/dev/null 2>&1; then
85
194
  echo "$health" | grep -q 'workframe_e2e' || warn "API health missing workframe_e2e"
86
195
  echo "$health" | grep -q 'dev_local_unsafe' || warn "API health missing dev_local_unsafe"
87
196
  ok "API health"
197
+ snapshot_code="$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${API_PORT}/api/snapshot" || true)"
198
+ [[ "$snapshot_code" == "401" ]] || fail "anonymous /api/snapshot returned ${snapshot_code:-<empty>} (expected 401)"
199
+ ok "anonymous /api/snapshot denied"
88
200
  fi
89
201
  dash_code="$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${UI_PORT}/hermes-dashboard/" || true)"
90
202
  [[ "$dash_code" == "403" ]] || warn "hermes-dashboard without session returned $dash_code (expected 403)"
@@ -92,14 +204,20 @@ else
92
204
  warn "curl not found — skipping HTTP checks"
93
205
  fi
94
206
 
95
- if command -v docker >/dev/null 2>&1 && docker inspect workframe-gateway >/dev/null 2>&1; then
96
- gw_env="$(docker inspect workframe-gateway --format '{{range .Config.Env}}{{println .}}{{end}}')"
207
+ if command -v docker >/dev/null 2>&1 && docker inspect "$(gateway_container_name)" >/dev/null 2>&1; then
208
+ gw_env="$(docker inspect "$(gateway_container_name)" --format '{{range .Config.Env}}{{println .}}{{end}}')"
97
209
  for marker in WORKFRAME_SUPERVISOR_TOKEN ZK_AUTH_ SMTP_PASS; do
98
210
  echo "$gw_env" | grep -q "$marker" && fail "gateway env contains $marker"
99
211
  done
100
212
  ok "gateway env allowlist"
101
213
  else
102
- warn "workframe-gateway not running — skipping docker inspect"
214
+ warn "$(gateway_container_name) not running — skipping docker inspect"
103
215
  fi
104
216
 
217
+ report_egress_posture
218
+
219
+ report_supervisor_constraints
220
+ report_broker_audit_schema
221
+ report_backup_restore_evidence
222
+
105
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")