create-workframe 0.1.5 → 0.1.6

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 (181) hide show
  1. package/.dockerignore +22 -22
  2. package/.gitignore +73 -73
  3. package/LICENSE +201 -201
  4. package/NOTICE +12 -12
  5. package/README.md +40 -40
  6. package/SECURITY.md +38 -38
  7. package/bin/create-workframe.js +2816 -2814
  8. package/bin/workframe.js +329 -329
  9. package/docs/workspace-instructions/WORKFRAME_DISCORD.md +20 -20
  10. package/docs/workspace-instructions/WORKFRAME_DOCUMENTS_AND_ARTIFACTS.md +20 -20
  11. package/docs/workspace-instructions/WORKFRAME_KANBAN.md +20 -20
  12. package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +21 -21
  13. package/docs/workspace-instructions/WORKFRAME_ROUTING.md +29 -29
  14. package/docs/workspace-instructions/WORKFRAME_TELEGRAM.md +19 -19
  15. package/package.json +64 -64
  16. package/profiles/README.md +15 -15
  17. package/profiles/architect/AGENTS.md +29 -29
  18. package/profiles/architect/SOUL.md +44 -44
  19. package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
  20. package/profiles/designer/AGENTS.md +26 -26
  21. package/profiles/designer/SOUL.md +31 -31
  22. package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
  23. package/profiles/dev/AGENTS.md +28 -28
  24. package/profiles/dev/SOUL.md +31 -31
  25. package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
  26. package/profiles/docs/AGENTS.md +27 -27
  27. package/profiles/docs/SOUL.md +30 -30
  28. package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
  29. package/profiles/research/AGENTS.md +26 -26
  30. package/profiles/research/SOUL.md +31 -31
  31. package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
  32. package/profiles/visionary/AGENTS.md +25 -25
  33. package/profiles/visionary/SOUL.md +31 -31
  34. package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
  35. package/profiles/workframe-agent/AGENTS.md +37 -37
  36. package/profiles/workframe-agent/SETUP.md +185 -185
  37. package/profiles/workframe-agent/SOUL.md +61 -61
  38. package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
  39. package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
  40. package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
  41. package/prompts/WORKFRAME_PROMPT_TEMPLATES.md +16 -16
  42. package/rules/.hermes.md +11 -11
  43. package/rules/AGENTS.md +22 -22
  44. package/rules/workspace-README.md +5 -5
  45. package/scripts/apply-update-workframe.sh +91 -21
  46. package/scripts/bundle-workframe-ui.mjs +83 -83
  47. package/scripts/create_workframe_scaffold.py +648 -648
  48. package/scripts/ensure-compose-host-paths.mjs +51 -51
  49. package/scripts/lib/install-identity.mjs +212 -212
  50. package/scripts/lib/workframe-registry.mjs +290 -290
  51. package/scripts/new-project.mjs +68 -68
  52. package/scripts/select_agent_pack.py +31 -31
  53. package/scripts/set-compose-public-url.mjs +92 -92
  54. package/scripts/sync-canonical-to-package.mjs +164 -164
  55. package/shared/WORKFRAME_AGENT_LIBRARY.md +31 -31
  56. package/shared/WORKFRAME_AGENT_OPERATIONS.md +29 -29
  57. package/shared/WORKFRAME_AGENT_PACKS.json +64 -64
  58. package/shared/WORKFRAME_AGENT_PACKS.yaml +20 -20
  59. package/shared/WORKFRAME_CHAT_PERMISSION_MODEL.md +20 -20
  60. package/shared/WORKFRAME_HANDOFF_SCHEMA.md +25 -25
  61. package/shared/WORKFRAME_SKILL_CURATION.md +27 -27
  62. package/workframe-api/Dockerfile +14 -14
  63. package/workframe-api/README.md +24 -24
  64. package/workframe-api/action_proxy.py +131 -131
  65. package/workframe-api/auth_rate_limit.py +49 -49
  66. package/workframe-api/credential_vault.py +445 -445
  67. package/workframe-api/data/avatar-catalog.json +41 -41
  68. package/workframe-api/data/logo-catalog.json +14 -14
  69. package/workframe-api/data/user-avatar-catalog.json +18 -18
  70. package/workframe-api/email_sender.py +220 -220
  71. package/workframe-api/google_auth.py +90 -90
  72. package/workframe-api/install_api.py +359 -359
  73. package/workframe-api/internal_proxy_auth.py +150 -150
  74. package/workframe-api/llm_proxy.py +280 -280
  75. package/workframe-api/oidc_jwt.py +108 -108
  76. package/workframe-api/package.json +12 -12
  77. package/workframe-api/platform_auth.py +194 -194
  78. package/workframe-api/profile_secret_policy.py +86 -86
  79. package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
  80. package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
  81. package/workframe-api/public/assets/index-DglUqFB_.js +8 -8
  82. package/workframe-api/public/index.html +12 -12
  83. package/workframe-api/requirements.txt +2 -2
  84. package/workframe-api/server.py +19866 -19866
  85. package/workframe-api/site_meta.py +271 -271
  86. package/workframe-api/stack_config.py +427 -427
  87. package/workframe-api/time-bind-chat.py +99 -99
  88. package/workframe-api/turn_credentials.py +226 -226
  89. package/workframe-api/updates.py +506 -506
  90. package/workframe-api/vault_kek.py +159 -159
  91. package/workframe-api/zk_auth.py +633 -633
  92. package/workframe-supervisor/Dockerfile +11 -11
  93. package/workframe-supervisor/profile_secret_policy.py +76 -76
  94. package/workframe-supervisor/server.py +860 -860
  95. package/workframe-ui/docker/nginx.conf +85 -85
  96. package/workframe-ui/public/assets/{arc-CnriPN3G.js → arc-DoYr2T5c.js} +1 -1
  97. package/workframe-ui/public/assets/architecture-7EHR7CIX-CmDi28hh.js +1 -0
  98. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DYYB1Ic2.js → architectureDiagram-3BPJPVTR-Clher08G.js} +1 -1
  99. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-Bn-fFOyj.js → blockDiagram-GPEHLZMM-Bdw9txrZ.js} +1 -1
  100. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BPPDtAnD.js → c4Diagram-AAUBKEIU-DlI_h_uo.js} +1 -1
  101. package/workframe-ui/public/assets/channel-DZYVoEdD.js +1 -0
  102. package/workframe-ui/public/assets/{chunk-2J33WTMH-Bz2CJiC3.js → chunk-2J33WTMH-ol-uwoeq.js} +1 -1
  103. package/workframe-ui/public/assets/{chunk-3OPIFGDE-B8aMeE1B.js → chunk-3OPIFGDE-7eDhBen0.js} +1 -1
  104. package/workframe-ui/public/assets/{chunk-4BX2VUAB-bLFHQTuT.js → chunk-4BX2VUAB-xPORD7Js.js} +1 -1
  105. package/workframe-ui/public/assets/{chunk-55IACEB6-BRxhx-lG.js → chunk-55IACEB6-Cwd7dr1z.js} +1 -1
  106. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BLyOgEJ8.js → chunk-5ZQYHXKU-Ckt4Sjy1.js} +1 -1
  107. package/workframe-ui/public/assets/{chunk-727SXJPM-DiuvBl7G.js → chunk-727SXJPM-sMk1op_G.js} +1 -1
  108. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-BE1_aBdC.js → chunk-AQP2D5EJ-CMgSvr6H.js} +1 -1
  109. package/workframe-ui/public/assets/{chunk-BSJP7CBP-BicGsmBs.js → chunk-BSJP7CBP-Dx7GN0T4.js} +1 -1
  110. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-C3VTns3d.js → chunk-CSCIHK7Q--OCu86u0.js} +1 -1
  111. package/workframe-ui/public/assets/{chunk-FMBD7UC4-DSiXY3bY.js → chunk-FMBD7UC4-CejmwJpz.js} +1 -1
  112. package/workframe-ui/public/assets/{chunk-KSCS5N6A-Cvw5s3fR.js → chunk-KSCS5N6A-CvOj2Yvp.js} +1 -1
  113. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-jJifGdIQ.js → chunk-L5ZTLDWV-B0nGBjLU.js} +1 -1
  114. package/workframe-ui/public/assets/{chunk-LZXEDZCA-Maee1aqo.js → chunk-LZXEDZCA-Bdzvu227.js} +2 -2
  115. package/workframe-ui/public/assets/{chunk-ND2GUHAM-C77N3VAM.js → chunk-ND2GUHAM-CGR07jTY.js} +1 -1
  116. package/workframe-ui/public/assets/{chunk-NZK2D7GU-BY68lrPB.js → chunk-NZK2D7GU-CBojDPNC.js} +1 -1
  117. package/workframe-ui/public/assets/{chunk-O5CBEL6O-D_c4kWzh.js → chunk-O5CBEL6O-DBin5MPF.js} +1 -1
  118. package/workframe-ui/public/assets/chunk-QZHKN3VN-DwiajAaK.js +1 -0
  119. package/workframe-ui/public/assets/chunk-WU5MYG2G-Bz8vA5s7.js +1 -0
  120. package/workframe-ui/public/assets/{chunk-XPW4576I-DlQgvqBp.js → chunk-XPW4576I-TD7I7pcb.js} +1 -1
  121. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DQiq8od0.js +1 -0
  122. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DQiq8od0.js +1 -0
  123. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CHCOHDSd.js → cose-bilkent-S5V4N54A-Bmm5x5xm.js} +1 -1
  124. package/workframe-ui/public/assets/{dagre-BM42HDAG-CKPcSYwU.js → dagre-BM42HDAG-zU2r0aq2.js} +1 -1
  125. package/workframe-ui/public/assets/{diagram-2AECGRRQ-C6qe0jP-.js → diagram-2AECGRRQ-AL01qukm.js} +1 -1
  126. package/workframe-ui/public/assets/{diagram-5GNKFQAL-DhQhrieU.js → diagram-5GNKFQAL-DKi1xSMg.js} +1 -1
  127. package/workframe-ui/public/assets/{diagram-KO2AKTUF-Bqf6Iw1r.js → diagram-KO2AKTUF-DQJDQ-f-.js} +1 -1
  128. package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt1ms2D2.js → diagram-LMA3HP47-B1oisGUj.js} +1 -1
  129. package/workframe-ui/public/assets/{diagram-OG6HWLK6-C0Rrxe1i.js → diagram-OG6HWLK6-DjpJKffk.js} +1 -1
  130. package/workframe-ui/public/assets/{dist-BLcLhTn6.js → dist-Jk20AnQG.js} +1 -1
  131. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-Cy1GbZVu.js → erDiagram-TEJ5UH35-Vu8RtLUf.js} +1 -1
  132. package/workframe-ui/public/assets/eventmodeling-FCH6USID-BXUnTe0c.js +1 -0
  133. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-DL3ALqDG.js → flowDiagram-I6XJVG4X-CHAXEhbR.js} +1 -1
  134. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-CMpwBhXQ.js → ganttDiagram-6RSMTGT7-DhiEkfH8.js} +1 -1
  135. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-C0mwHUXL.js → gitGraph-WXDBUCRP-Cuq44XvU.js} +1 -1
  136. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-6gzhWaF0.js → gitGraphDiagram-PVQCEYII-B7kgReRt.js} +1 -1
  137. package/workframe-ui/public/assets/index-72-j0sA5.js +129 -0
  138. package/workframe-ui/public/assets/index-auJ0bLMU.css +1 -0
  139. package/workframe-ui/public/assets/{info-J43DQDTF-Bb_wZTxU.js → info-J43DQDTF-oV4b3M4f.js} +1 -1
  140. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-601evYFV.js → infoDiagram-5YYISTIA-CthN4b6o.js} +1 -1
  141. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BYtF-wHm.js → ishikawaDiagram-YF4QCWOH-oEGS3TKg.js} +1 -1
  142. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-BMizyHzC.js → journeyDiagram-JHISSGLW-CXJsWmKF.js} +1 -1
  143. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CLLJsdnh.js → kanban-definition-UN3LZRKU-DTPo8yHW.js} +1 -1
  144. package/workframe-ui/public/assets/{line-BFiSvu-q.js → line-Bm9Pmcnz.js} +1 -1
  145. package/workframe-ui/public/assets/{linear-D6HYZ18_.js → linear-Cn-uD6eX.js} +1 -1
  146. package/workframe-ui/public/assets/{mermaid-parser.core-Cid5yDBQ.js → mermaid-parser.core-BBVSTQZ4.js} +2 -2
  147. package/workframe-ui/public/assets/{mermaid.core-DqIiju5Z.js → mermaid.core-DE668nUy.js} +3 -3
  148. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-Bg7xJdsz.js → mindmap-definition-RKZ34NQL-4qVsiwCf.js} +1 -1
  149. package/workframe-ui/public/assets/{packet-YPE3B663-1ebGt86D.js → packet-YPE3B663-CqJeoiFG.js} +1 -1
  150. package/workframe-ui/public/assets/{pie-LRSECV5Y-DJ9wOBC_.js → pie-LRSECV5Y-DcOqVssg.js} +1 -1
  151. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-BmK96Aus.js → pieDiagram-4H26LBE5-CosMnpAM.js} +1 -1
  152. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-ByUqtWPZ.js → quadrantDiagram-W4KKPZXB-DtsnIK98.js} +1 -1
  153. package/workframe-ui/public/assets/{radar-GUYGQ44K-DN-Hkt96.js → radar-GUYGQ44K-BqLpJuWc.js} +1 -1
  154. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ByU2r8gb.js → requirementDiagram-4Y6WPE33-Pb7cuX8_.js} +1 -1
  155. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-elyWa0GK.js → sankeyDiagram-5OEKKPKP-BJBlMM1m.js} +1 -1
  156. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-CzZwsAiK.js → sequenceDiagram-3UESZ5HK-i4HnjP39.js} +1 -1
  157. package/workframe-ui/public/assets/{src-5vgsM_dl.js → src-DIhmc_XG.js} +1 -1
  158. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-B0O5w5lJ.js → stateDiagram-AJRCARHV-Ba2iClPT.js} +1 -1
  159. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-aUpqRAhy.js +1 -0
  160. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-oyqa5w5y.js → timeline-definition-PNZ67QCA-CL4X9vSF.js} +1 -1
  161. package/workframe-ui/public/assets/{treeView-BLDUP644-MCV12-eP.js → treeView-BLDUP644-BJrjnbMj.js} +1 -1
  162. package/workframe-ui/public/assets/{treemap-LRROVOQU-DiA1P9n5.js → treemap-LRROVOQU-CN6H54RF.js} +1 -1
  163. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CO_4x7OR.js → vennDiagram-CIIHVFJN-Cee6sbUy.js} +1 -1
  164. package/workframe-ui/public/assets/{wardley-L42UT6IY-CmQNv6Xv.js → wardley-L42UT6IY-D0cXq60c.js} +1 -1
  165. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-CJxDkbIj.js → wardleyDiagram-YWT4CUSO-CaH9XkCx.js} +1 -1
  166. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-B02MxgzG.js → xychartDiagram-2RQKCTM6-BEisgFoI.js} +1 -1
  167. package/workframe-ui/public/favicon.svg +7 -7
  168. package/workframe-ui/public/icons.svg +24 -24
  169. package/workframe-ui/public/index.html +47 -47
  170. package/workframe-ui/public/manifest.webmanifest +18 -18
  171. package/workframe-ui/public/workframe-config.json +3 -3
  172. package/workframe-ui/public/assets/architecture-7EHR7CIX-CFhr7xAe.js +0 -1
  173. package/workframe-ui/public/assets/channel-Dok_-T0o.js +0 -1
  174. package/workframe-ui/public/assets/chunk-QZHKN3VN-C3wwLrTa.js +0 -1
  175. package/workframe-ui/public/assets/chunk-WU5MYG2G-GFFSg2kf.js +0 -1
  176. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DKbGfT5Z.js +0 -1
  177. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DKbGfT5Z.js +0 -1
  178. package/workframe-ui/public/assets/eventmodeling-FCH6USID-C_PrceHe.js +0 -1
  179. package/workframe-ui/public/assets/index-DyO6K0TD.css +0 -1
  180. package/workframe-ui/public/assets/index-yvpSUs8T.js +0 -133
  181. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-RXZRaSgT.js +0 -1
@@ -1,25 +1,25 @@
1
- # Workframe Handoff Schema
2
-
3
- Every completed specialist task should include:
4
-
5
- Required
6
- - summary: what was done
7
- - changed_files: absolute or repo-relative paths
8
- - next_action: concrete next step
9
- - owner: next responsible profile/person
10
-
11
- Recommended
12
- - decisions: key choices made
13
- - assumptions: unresolved assumptions
14
- - blockers: explicit question if blocked
15
- - evidence: links/refs/tests for verification
16
-
17
- Heartbeat schema (for long-running tasks)
18
- - state: phase name
19
- - progress: short status
20
- - eta: rough estimate
21
- - risk: only if material risk changed
22
-
23
- Storage
24
- - heartbeat/handoff in Kanban comments
25
- - durable result in project files
1
+ # Workframe Handoff Schema
2
+
3
+ Every completed specialist task should include:
4
+
5
+ Required
6
+ - summary: what was done
7
+ - changed_files: absolute or repo-relative paths
8
+ - next_action: concrete next step
9
+ - owner: next responsible profile/person
10
+
11
+ Recommended
12
+ - decisions: key choices made
13
+ - assumptions: unresolved assumptions
14
+ - blockers: explicit question if blocked
15
+ - evidence: links/refs/tests for verification
16
+
17
+ Heartbeat schema (for long-running tasks)
18
+ - state: phase name
19
+ - progress: short status
20
+ - eta: rough estimate
21
+ - risk: only if material risk changed
22
+
23
+ Storage
24
+ - heartbeat/handoff in Kanban comments
25
+ - durable result in project files
@@ -1,27 +1,27 @@
1
- # Workframe Skill and Tool Curation
2
-
3
- Purpose
4
- - enforce lean specialization and avoid context bloat.
5
-
6
- Skill matrix (recommended baseline)
7
-
8
- | Profile | Mandatory (recommended) | Optional (recommended) | Avoid |
9
- |---|---|---|---|
10
- | {nativeProfileSlug} (project agent) | kanban-orchestrator | writing-plans, plan | domain-heavy implementation skills |
11
- | visionary | writing-plans | arxiv, blogwatcher | low-level debugging stacks |
12
- | architect | writing-plans | systematic-debugging, github-code-review | generic creative bundles |
13
- | docs | humanizer | hermes-agent-skill-authoring | infra-heavy ops bundles |
14
- | dev | test-driven-development, systematic-debugging | python-debugpy, requesting-code-review | broad strategy skills |
15
- | research | arxiv | llm-wiki, blogwatcher | unrelated creative generators |
16
- | designer | sketch | popular-web-designs, architecture-diagram | deep backend/debug toolchains |
17
-
18
- Tool curation rules
19
- 1) Give each profile only tools it uses weekly.
20
- 2) Shared essentials should be small.
21
- 3) Add a skill only after repeated need.
22
- 4) Remove stale skills quarterly.
23
-
24
- Install-time selection
25
- - default to the `native` starter pack unless a larger reference pack is explicitly requested
26
- - reference packs remain available (`core` / `product` / `engineering` / `full`)
27
- - concierge can request profile add/remove later with owner approval.
1
+ # Workframe Skill and Tool Curation
2
+
3
+ Purpose
4
+ - enforce lean specialization and avoid context bloat.
5
+
6
+ Skill matrix (recommended baseline)
7
+
8
+ | Profile | Mandatory (recommended) | Optional (recommended) | Avoid |
9
+ |---|---|---|---|
10
+ | {nativeProfileSlug} (project agent) | kanban-orchestrator | writing-plans, plan | domain-heavy implementation skills |
11
+ | visionary | writing-plans | arxiv, blogwatcher | low-level debugging stacks |
12
+ | architect | writing-plans | systematic-debugging, github-code-review | generic creative bundles |
13
+ | docs | humanizer | hermes-agent-skill-authoring | infra-heavy ops bundles |
14
+ | dev | test-driven-development, systematic-debugging | python-debugpy, requesting-code-review | broad strategy skills |
15
+ | research | arxiv | llm-wiki, blogwatcher | unrelated creative generators |
16
+ | designer | sketch | popular-web-designs, architecture-diagram | deep backend/debug toolchains |
17
+
18
+ Tool curation rules
19
+ 1) Give each profile only tools it uses weekly.
20
+ 2) Shared essentials should be small.
21
+ 3) Add a skill only after repeated need.
22
+ 4) Remove stale skills quarterly.
23
+
24
+ Install-time selection
25
+ - default to the `native` starter pack unless a larger reference pack is explicitly requested
26
+ - reference packs remain available (`core` / `product` / `engineering` / `full`)
27
+ - concierge can request profile add/remove later with owner approval.
@@ -1,14 +1,14 @@
1
- FROM python:3-alpine
2
-
3
- RUN apk add --no-cache docker-cli docker-cli-compose bash nodejs npm
4
-
5
- WORKDIR /app
6
-
7
- COPY requirements.txt .
8
- RUN pip install --no-cache-dir -r requirements.txt
9
-
10
- COPY . .
11
-
12
- EXPOSE 8080
13
-
14
- CMD ["python3", "server.py"]
1
+ FROM python:3-alpine
2
+
3
+ RUN apk add --no-cache docker-cli docker-cli-compose bash nodejs npm
4
+
5
+ WORKDIR /app
6
+
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+
10
+ COPY . .
11
+
12
+ EXPOSE 8080
13
+
14
+ CMD ["python3", "server.py"]
@@ -1,24 +1,24 @@
1
- # Workframe API Service
2
-
3
- HTTP API for the Workframe UI. See [docs/VERSION.md](../../docs/VERSION.md) for release version.
4
-
5
- ## Local (outside Docker)
6
-
7
- ```bash
8
- cd services/workframe-api
9
- python3 -m venv .venv
10
- . .venv/bin/activate # Windows: .venv\Scripts\activate
11
- pip install -r requirements.txt
12
- HOST=0.0.0.0 PORT=8080 HERMES_DATA=/opt/data WORKSPACE=/workspace python3 server.py
13
- ```
14
-
15
- ## Runtime state (not committed)
16
-
17
- - `data/*.db`, vault files under `WORKFRAME_API_DATA_DIR`
18
- - Hermes `Agents/` tree on mounted volume
19
-
20
- ## Docs
21
-
22
- - [Operations](../../docs/public/operations.md)
23
- - [Security](../../docs/public/security.md)
24
- - [API reference](../../docs/public/api-reference.md)
1
+ # Workframe API Service
2
+
3
+ HTTP API for the Workframe UI. See [docs/VERSION.md](../../docs/VERSION.md) for release version.
4
+
5
+ ## Local (outside Docker)
6
+
7
+ ```bash
8
+ cd services/workframe-api
9
+ python3 -m venv .venv
10
+ . .venv/bin/activate # Windows: .venv\Scripts\activate
11
+ pip install -r requirements.txt
12
+ HOST=0.0.0.0 PORT=8080 HERMES_DATA=/opt/data WORKSPACE=/workspace python3 server.py
13
+ ```
14
+
15
+ ## Runtime state (not committed)
16
+
17
+ - `data/*.db`, vault files under `WORKFRAME_API_DATA_DIR`
18
+ - Hermes `Agents/` tree on mounted volume
19
+
20
+ ## Docs
21
+
22
+ - [Operations](../../docs/public/operations.md)
23
+ - [Security](../../docs/public/security.md)
24
+ - [API reference](../../docs/public/api-reference.md)
@@ -1,131 +1,131 @@
1
- """Internal action proxy — dev/tool PATs stay in vault; Hermes sends lease tokens."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import re
7
- import urllib.error
8
- import urllib.request
9
- from http.server import BaseHTTPRequestHandler
10
- from typing import Any, Callable
11
-
12
- import llm_proxy
13
- import turn_credentials
14
-
15
- UPSTREAM_BASE: dict[str, str] = {
16
- "github": "https://api.github.com",
17
- "vercel": "https://api.vercel.com",
18
- "netlify": "https://api.netlify.com/api/v1",
19
- }
20
-
21
- PROXY_PATH_RE = re.compile(r"^/internal/action/([a-z0-9_-]+)(/.*)?$", re.IGNORECASE)
22
-
23
-
24
- def upstream_auth_header(provider: str, secret: str) -> dict[str, str]:
25
- provider = str(provider or "").strip().lower()
26
- secret = str(secret or "").strip()
27
- if provider == "github":
28
- return {"Authorization": f"Bearer {secret}", "Accept": "application/vnd.github+json"}
29
- if provider == "netlify":
30
- return {"Authorization": f"Bearer {secret}"}
31
- return {"Authorization": f"Bearer {secret}"}
32
-
33
-
34
- def forward_request(
35
- provider: str,
36
- subpath: str,
37
- method: str,
38
- headers: dict[str, str],
39
- body: bytes | None,
40
- *,
41
- resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
42
- ) -> tuple[int, dict[str, str], bytes]:
43
- provider = str(provider or "").strip().lower()
44
- base = UPSTREAM_BASE.get(provider)
45
- if not base:
46
- return 404, {"Content-Type": "application/json"}, json.dumps({"error": "unknown provider"}).encode()
47
-
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
- return (
58
- profile_status,
59
- {"Content-Type": "application/json"},
60
- json.dumps({"error": profile_err}).encode(),
61
- )
62
-
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()
66
-
67
- path = subpath if subpath.startswith("/") else f"/{subpath}"
68
- url = f"{base.rstrip('/')}{path}"
69
- upstream_headers = {
70
- k: v
71
- for k, v in headers.items()
72
- if k.lower()
73
- not in {
74
- "host",
75
- "connection",
76
- "content-length",
77
- "authorization",
78
- "x-api-key",
79
- }
80
- }
81
- upstream_headers.update(upstream_auth_header(provider, secret))
82
-
83
- req = urllib.request.Request(url, data=body, headers=upstream_headers, method=method.upper())
84
- try:
85
- with urllib.request.urlopen(req, timeout=600) as resp:
86
- resp_body = resp.read()
87
- out_headers = {"Content-Type": resp.headers.get("Content-Type", "application/octet-stream")}
88
- return resp.status, out_headers, resp_body
89
- except urllib.error.HTTPError as exc:
90
- raw = exc.read()
91
- out_headers = {"Content-Type": exc.headers.get("Content-Type", "application/json")}
92
- return exc.code, out_headers, raw
93
-
94
-
95
- def handle_proxy_request(
96
- handler: BaseHTTPRequestHandler,
97
- path: str,
98
- method: str,
99
- body: bytes | None,
100
- *,
101
- resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
102
- ) -> bool:
103
- ok, err = llm_proxy.authorize_internal_proxy(handler)
104
- if not ok:
105
- handler.send_response(403)
106
- handler.send_header("Content-Type", "application/json")
107
- handler.end_headers()
108
- handler.wfile.write(json.dumps({"error": err or "internal only"}).encode())
109
- return True
110
-
111
- match = PROXY_PATH_RE.match(path)
112
- if not match:
113
- return False
114
-
115
- provider = match.group(1).lower()
116
- subpath = match.group(2) or "/"
117
- headers = {k: v for k, v in handler.headers.items()}
118
- status, out_headers, resp_body = forward_request(
119
- provider,
120
- subpath,
121
- method,
122
- headers,
123
- body,
124
- resolve_secret=resolve_secret,
125
- )
126
- handler.send_response(status)
127
- for key, value in out_headers.items():
128
- handler.send_header(key, value)
129
- handler.end_headers()
130
- handler.wfile.write(resp_body)
131
- return True
1
+ """Internal action proxy — dev/tool PATs stay in vault; Hermes sends lease tokens."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import urllib.error
8
+ import urllib.request
9
+ from http.server import BaseHTTPRequestHandler
10
+ from typing import Any, Callable
11
+
12
+ import llm_proxy
13
+ import turn_credentials
14
+
15
+ UPSTREAM_BASE: dict[str, str] = {
16
+ "github": "https://api.github.com",
17
+ "vercel": "https://api.vercel.com",
18
+ "netlify": "https://api.netlify.com/api/v1",
19
+ }
20
+
21
+ PROXY_PATH_RE = re.compile(r"^/internal/action/([a-z0-9_-]+)(/.*)?$", re.IGNORECASE)
22
+
23
+
24
+ def upstream_auth_header(provider: str, secret: str) -> dict[str, str]:
25
+ provider = str(provider or "").strip().lower()
26
+ secret = str(secret or "").strip()
27
+ if provider == "github":
28
+ return {"Authorization": f"Bearer {secret}", "Accept": "application/vnd.github+json"}
29
+ if provider == "netlify":
30
+ return {"Authorization": f"Bearer {secret}"}
31
+ return {"Authorization": f"Bearer {secret}"}
32
+
33
+
34
+ def forward_request(
35
+ provider: str,
36
+ subpath: str,
37
+ method: str,
38
+ headers: dict[str, str],
39
+ body: bytes | None,
40
+ *,
41
+ resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
42
+ ) -> tuple[int, dict[str, str], bytes]:
43
+ provider = str(provider or "").strip().lower()
44
+ base = UPSTREAM_BASE.get(provider)
45
+ if not base:
46
+ return 404, {"Content-Type": "application/json"}, json.dumps({"error": "unknown provider"}).encode()
47
+
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
+ return (
58
+ profile_status,
59
+ {"Content-Type": "application/json"},
60
+ json.dumps({"error": profile_err}).encode(),
61
+ )
62
+
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()
66
+
67
+ path = subpath if subpath.startswith("/") else f"/{subpath}"
68
+ url = f"{base.rstrip('/')}{path}"
69
+ upstream_headers = {
70
+ k: v
71
+ for k, v in headers.items()
72
+ if k.lower()
73
+ not in {
74
+ "host",
75
+ "connection",
76
+ "content-length",
77
+ "authorization",
78
+ "x-api-key",
79
+ }
80
+ }
81
+ upstream_headers.update(upstream_auth_header(provider, secret))
82
+
83
+ req = urllib.request.Request(url, data=body, headers=upstream_headers, method=method.upper())
84
+ try:
85
+ with urllib.request.urlopen(req, timeout=600) as resp:
86
+ resp_body = resp.read()
87
+ out_headers = {"Content-Type": resp.headers.get("Content-Type", "application/octet-stream")}
88
+ return resp.status, out_headers, resp_body
89
+ except urllib.error.HTTPError as exc:
90
+ raw = exc.read()
91
+ out_headers = {"Content-Type": exc.headers.get("Content-Type", "application/json")}
92
+ return exc.code, out_headers, raw
93
+
94
+
95
+ def handle_proxy_request(
96
+ handler: BaseHTTPRequestHandler,
97
+ path: str,
98
+ method: str,
99
+ body: bytes | None,
100
+ *,
101
+ resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
102
+ ) -> bool:
103
+ ok, err = llm_proxy.authorize_internal_proxy(handler)
104
+ if not ok:
105
+ handler.send_response(403)
106
+ handler.send_header("Content-Type", "application/json")
107
+ handler.end_headers()
108
+ handler.wfile.write(json.dumps({"error": err or "internal only"}).encode())
109
+ return True
110
+
111
+ match = PROXY_PATH_RE.match(path)
112
+ if not match:
113
+ return False
114
+
115
+ provider = match.group(1).lower()
116
+ subpath = match.group(2) or "/"
117
+ headers = {k: v for k, v in handler.headers.items()}
118
+ status, out_headers, resp_body = forward_request(
119
+ provider,
120
+ subpath,
121
+ method,
122
+ headers,
123
+ body,
124
+ resolve_secret=resolve_secret,
125
+ )
126
+ handler.send_response(status)
127
+ for key, value in out_headers.items():
128
+ handler.send_header(key, value)
129
+ handler.end_headers()
130
+ handler.wfile.write(resp_body)
131
+ return True
@@ -1,49 +1,49 @@
1
- """In-process auth endpoint throttle — trust boundary (0022 N5)."""
2
- from __future__ import annotations
3
-
4
- import time
5
- from collections import deque
6
-
7
- _BUCKETS: dict[str, deque[float]] = {}
8
- _MINUTE_WINDOW = 60.0
9
- _MINUTE_MAX = 5
10
- _HOUR_WINDOW = 3600.0
11
- _HOUR_MAX = 20
12
-
13
-
14
- def _bucket_key(kind: str, client_ip: str, email: str) -> str:
15
- return f"{kind}:{client_ip}:{email.strip().lower()}"
16
-
17
-
18
- def _prune(queue: deque[float], now: float, window: float) -> None:
19
- while queue and now - queue[0] > window:
20
- queue.popleft()
21
-
22
-
23
- def allow_auth_request(kind: str, client_ip: str, email: str) -> bool:
24
- now = time.time()
25
- key = _bucket_key(kind, client_ip, email)
26
- queue = _BUCKETS.setdefault(key, deque())
27
- _prune(queue, now, _HOUR_WINDOW)
28
- if len(queue) >= _HOUR_MAX:
29
- return False
30
- recent = deque(queue)
31
- _prune(recent, now, _MINUTE_WINDOW)
32
- if len(recent) >= _MINUTE_MAX:
33
- return False
34
- queue.append(now)
35
- return True
36
-
37
-
38
- def reset_for_tests() -> None:
39
- _BUCKETS.clear()
40
-
41
-
42
- if __name__ == "__main__":
43
- reset_for_tests()
44
- ip = "203.0.113.1"
45
- email = "a@example.com"
46
- for _ in range(_MINUTE_MAX):
47
- assert allow_auth_request("start", ip, email)
48
- assert not allow_auth_request("start", ip, email)
49
- print("auth_rate_limit ok")
1
+ """In-process auth endpoint throttle — trust boundary (0022 N5)."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+ from collections import deque
6
+
7
+ _BUCKETS: dict[str, deque[float]] = {}
8
+ _MINUTE_WINDOW = 60.0
9
+ _MINUTE_MAX = 5
10
+ _HOUR_WINDOW = 3600.0
11
+ _HOUR_MAX = 20
12
+
13
+
14
+ def _bucket_key(kind: str, client_ip: str, email: str) -> str:
15
+ return f"{kind}:{client_ip}:{email.strip().lower()}"
16
+
17
+
18
+ def _prune(queue: deque[float], now: float, window: float) -> None:
19
+ while queue and now - queue[0] > window:
20
+ queue.popleft()
21
+
22
+
23
+ def allow_auth_request(kind: str, client_ip: str, email: str) -> bool:
24
+ now = time.time()
25
+ key = _bucket_key(kind, client_ip, email)
26
+ queue = _BUCKETS.setdefault(key, deque())
27
+ _prune(queue, now, _HOUR_WINDOW)
28
+ if len(queue) >= _HOUR_MAX:
29
+ return False
30
+ recent = deque(queue)
31
+ _prune(recent, now, _MINUTE_WINDOW)
32
+ if len(recent) >= _MINUTE_MAX:
33
+ return False
34
+ queue.append(now)
35
+ return True
36
+
37
+
38
+ def reset_for_tests() -> None:
39
+ _BUCKETS.clear()
40
+
41
+
42
+ if __name__ == "__main__":
43
+ reset_for_tests()
44
+ ip = "203.0.113.1"
45
+ email = "a@example.com"
46
+ for _ in range(_MINUTE_MAX):
47
+ assert allow_auth_request("start", ip, email)
48
+ assert not allow_auth_request("start", ip, email)
49
+ print("auth_rate_limit ok")