create-workframe 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +22 -22
- package/.gitignore +73 -73
- package/LICENSE +201 -201
- package/NOTICE +12 -12
- package/README.md +40 -40
- package/SECURITY.md +38 -38
- package/bin/create-workframe.js +2816 -2814
- package/bin/workframe.js +329 -329
- package/docs/workspace-instructions/WORKFRAME_DISCORD.md +20 -20
- package/docs/workspace-instructions/WORKFRAME_DOCUMENTS_AND_ARTIFACTS.md +20 -20
- package/docs/workspace-instructions/WORKFRAME_KANBAN.md +20 -20
- package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +21 -21
- package/docs/workspace-instructions/WORKFRAME_ROUTING.md +29 -29
- package/docs/workspace-instructions/WORKFRAME_TELEGRAM.md +19 -19
- package/package.json +64 -64
- package/profiles/README.md +15 -15
- package/profiles/architect/AGENTS.md +29 -29
- package/profiles/architect/SOUL.md +44 -44
- package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/designer/AGENTS.md +26 -26
- package/profiles/designer/SOUL.md +31 -31
- package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/dev/AGENTS.md +28 -28
- package/profiles/dev/SOUL.md +31 -31
- package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/docs/AGENTS.md +27 -27
- package/profiles/docs/SOUL.md +30 -30
- package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/research/AGENTS.md +26 -26
- package/profiles/research/SOUL.md +31 -31
- package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/visionary/AGENTS.md +25 -25
- package/profiles/visionary/SOUL.md +31 -31
- package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/workframe-agent/AGENTS.md +37 -37
- package/profiles/workframe-agent/SETUP.md +185 -185
- package/profiles/workframe-agent/SOUL.md +61 -61
- package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
- package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
- package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
- package/prompts/WORKFRAME_PROMPT_TEMPLATES.md +16 -16
- package/rules/.hermes.md +11 -11
- package/rules/AGENTS.md +22 -22
- package/rules/workspace-README.md +5 -5
- package/scripts/apply-update-workframe.sh +91 -21
- package/scripts/bundle-workframe-ui.mjs +83 -83
- package/scripts/create_workframe_scaffold.py +648 -648
- package/scripts/ensure-compose-host-paths.mjs +51 -51
- package/scripts/lib/install-identity.mjs +212 -212
- package/scripts/lib/workframe-registry.mjs +290 -290
- package/scripts/new-project.mjs +68 -68
- package/scripts/select_agent_pack.py +31 -31
- package/scripts/set-compose-public-url.mjs +92 -92
- package/scripts/sync-canonical-to-package.mjs +164 -164
- package/shared/WORKFRAME_AGENT_LIBRARY.md +31 -31
- package/shared/WORKFRAME_AGENT_OPERATIONS.md +29 -29
- package/shared/WORKFRAME_AGENT_PACKS.json +64 -64
- package/shared/WORKFRAME_AGENT_PACKS.yaml +20 -20
- package/shared/WORKFRAME_CHAT_PERMISSION_MODEL.md +20 -20
- package/shared/WORKFRAME_HANDOFF_SCHEMA.md +25 -25
- package/shared/WORKFRAME_SKILL_CURATION.md +27 -27
- package/workframe-api/Dockerfile +14 -14
- package/workframe-api/README.md +24 -24
- package/workframe-api/action_proxy.py +131 -131
- package/workframe-api/auth_rate_limit.py +49 -49
- package/workframe-api/credential_vault.py +445 -445
- package/workframe-api/data/avatar-catalog.json +41 -41
- package/workframe-api/data/logo-catalog.json +14 -14
- package/workframe-api/data/user-avatar-catalog.json +18 -18
- package/workframe-api/email_sender.py +220 -220
- package/workframe-api/google_auth.py +90 -90
- package/workframe-api/install_api.py +359 -359
- package/workframe-api/internal_proxy_auth.py +150 -150
- package/workframe-api/llm_proxy.py +280 -280
- package/workframe-api/oidc_jwt.py +108 -108
- package/workframe-api/package.json +12 -12
- package/workframe-api/platform_auth.py +194 -194
- package/workframe-api/profile_secret_policy.py +86 -86
- package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
- package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
- package/workframe-api/public/assets/index-DglUqFB_.js +8 -8
- package/workframe-api/public/index.html +12 -12
- package/workframe-api/requirements.txt +2 -2
- package/workframe-api/server.py +20295 -19866
- package/workframe-api/site_meta.py +271 -271
- package/workframe-api/stack_config.py +435 -427
- package/workframe-api/test_ensure_native_profile.py +29 -0
- package/workframe-api/test_oauth_llm.py +58 -0
- package/workframe-api/test_provider_model_surface.py +31 -0
- package/workframe-api/test_stack_config_install_smtp.py +51 -0
- package/workframe-api/time-bind-chat.py +99 -99
- package/workframe-api/turn_credentials.py +226 -226
- package/workframe-api/updates.py +506 -506
- package/workframe-api/vault_kek.py +159 -159
- package/workframe-api/zk_auth.py +633 -633
- package/workframe-supervisor/Dockerfile +11 -11
- package/workframe-supervisor/profile_secret_policy.py +76 -76
- package/workframe-supervisor/server.py +860 -860
- package/workframe-ui/docker/nginx.conf +85 -85
- package/workframe-ui/public/assets/{arc-CnriPN3G.js → arc-Cn-lt9fT.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CCMNXHod.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DYYB1Ic2.js → architectureDiagram-3BPJPVTR-qXoGCqfH.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-Bn-fFOyj.js → blockDiagram-GPEHLZMM-DoopFlWj.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BPPDtAnD.js → c4Diagram-AAUBKEIU-BRa7hfzE.js} +1 -1
- package/workframe-ui/public/assets/channel-CtFV3sE3.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Bz2CJiC3.js → chunk-2J33WTMH-Cl33E2hh.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-B8aMeE1B.js → chunk-3OPIFGDE-BRywYZQO.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-bLFHQTuT.js → chunk-4BX2VUAB-CI3xzp-0.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-BRxhx-lG.js → chunk-55IACEB6-Bkf3Ye3F.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BLyOgEJ8.js → chunk-5ZQYHXKU-C5RN3hUT.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-DiuvBl7G.js → chunk-727SXJPM-BaY0yDwt.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-BE1_aBdC.js → chunk-AQP2D5EJ-VMMBaDqd.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-BicGsmBs.js → chunk-BSJP7CBP-DlNZ1UV3.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-C3VTns3d.js → chunk-CSCIHK7Q-tT2HyVy0.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-DSiXY3bY.js → chunk-FMBD7UC4-CyACRaUT.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-Cvw5s3fR.js → chunk-KSCS5N6A-seK5WAZs.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-jJifGdIQ.js → chunk-L5ZTLDWV-r5cyG5AH.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-Maee1aqo.js → chunk-LZXEDZCA-SnbeVxsJ.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-C77N3VAM.js → chunk-ND2GUHAM-zbFgEXSJ.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-BY68lrPB.js → chunk-NZK2D7GU-UhZc8kOk.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-D_c4kWzh.js → chunk-O5CBEL6O-DHvu2iZ-.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-B1g7l1_B.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-B_qfZ6QP.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-DlQgvqBp.js → chunk-XPW4576I-CCPNKbzG.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CJoeCNq6.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CJoeCNq6.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CHCOHDSd.js → cose-bilkent-S5V4N54A-D1CLe6Y0.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-CKPcSYwU.js → dagre-BM42HDAG-DYHcGVs_.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-C6qe0jP-.js → diagram-2AECGRRQ-DVUkJ-II.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-DhQhrieU.js → diagram-5GNKFQAL-C9Y1Acc2.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Bqf6Iw1r.js → diagram-KO2AKTUF-Db3zez-n.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt1ms2D2.js → diagram-LMA3HP47-BaOR5sIr.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-C0Rrxe1i.js → diagram-OG6HWLK6-fLBzMY5W.js} +1 -1
- package/workframe-ui/public/assets/{dist-BLcLhTn6.js → dist-uZYeD3o1.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-Cy1GbZVu.js → erDiagram-TEJ5UH35-BoCxDSyE.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Bw910jho.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-DL3ALqDG.js → flowDiagram-I6XJVG4X-BnYa3CAj.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-CMpwBhXQ.js → ganttDiagram-6RSMTGT7-Dm_e_Kaa.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-C0mwHUXL.js → gitGraph-WXDBUCRP-CBAj8ntv.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-6gzhWaF0.js → gitGraphDiagram-PVQCEYII-D5auJjJh.js} +1 -1
- package/workframe-ui/public/assets/index-DHLQNAXd.js +129 -0
- package/workframe-ui/public/assets/index-auJ0bLMU.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-Bb_wZTxU.js → info-J43DQDTF-BNMADxE2.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-601evYFV.js → infoDiagram-5YYISTIA-C8c45T4J.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BYtF-wHm.js → ishikawaDiagram-YF4QCWOH-CyWuHB4e.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-BMizyHzC.js → journeyDiagram-JHISSGLW-B43pCuDT.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CLLJsdnh.js → kanban-definition-UN3LZRKU-Dyn480BI.js} +1 -1
- package/workframe-ui/public/assets/{line-BFiSvu-q.js → line-CNtIbnmt.js} +1 -1
- package/workframe-ui/public/assets/{linear-D6HYZ18_.js → linear-pmSg_4uV.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Cid5yDBQ.js → mermaid-parser.core-BX4tnojE.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-DqIiju5Z.js → mermaid.core-BtpBva5o.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-Bg7xJdsz.js → mindmap-definition-RKZ34NQL-DcMcB_69.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-1ebGt86D.js → packet-YPE3B663-KSzK9WlD.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DJ9wOBC_.js → pie-LRSECV5Y-DLOH6Mg8.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-BmK96Aus.js → pieDiagram-4H26LBE5-BhrX2aGk.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-ByUqtWPZ.js → quadrantDiagram-W4KKPZXB-B5dDjbTX.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-DN-Hkt96.js → radar-GUYGQ44K-BPdjqZiu.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ByU2r8gb.js → requirementDiagram-4Y6WPE33-21Dq5e0N.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-elyWa0GK.js → sankeyDiagram-5OEKKPKP-CiA99dbO.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-CzZwsAiK.js → sequenceDiagram-3UESZ5HK-Bwpui7_x.js} +1 -1
- package/workframe-ui/public/assets/{src-5vgsM_dl.js → src-B3Axsi1U.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-B0O5w5lJ.js → stateDiagram-AJRCARHV-C76qwtE8.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-Dn0CDWJD.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-oyqa5w5y.js → timeline-definition-PNZ67QCA-CufInOyk.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-MCV12-eP.js → treeView-BLDUP644-C4sxh0R-.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-DiA1P9n5.js → treemap-LRROVOQU-BjV72ANV.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CO_4x7OR.js → vennDiagram-CIIHVFJN-DgsdR0Rl.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-CmQNv6Xv.js → wardley-L42UT6IY-DZ279G_t.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-CJxDkbIj.js → wardleyDiagram-YWT4CUSO-Bde25kvz.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-B02MxgzG.js → xychartDiagram-2RQKCTM6-B8VnS2Pb.js} +1 -1
- package/workframe-ui/public/favicon.svg +7 -7
- package/workframe-ui/public/icons.svg +24 -24
- package/workframe-ui/public/index.html +47 -47
- package/workframe-ui/public/manifest.webmanifest +18 -18
- package/workframe-ui/public/workframe-config.json +3 -3
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CFhr7xAe.js +0 -1
- package/workframe-ui/public/assets/channel-Dok_-T0o.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C3wwLrTa.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-GFFSg2kf.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DKbGfT5Z.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DKbGfT5Z.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-C_PrceHe.js +0 -1
- package/workframe-ui/public/assets/index-DyO6K0TD.css +0 -1
- package/workframe-ui/public/assets/index-yvpSUs8T.js +0 -133
- 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.
|
package/workframe-api/Dockerfile
CHANGED
|
@@ -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"]
|
package/workframe-api/README.md
CHANGED
|
@@ -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")
|