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.
- package/README.md +1 -1
- package/bin/create-workframe.js +2 -2
- package/bin/workframe.js +130 -2
- package/docs/security.md +85 -0
- package/package.json +1 -1
- package/scripts/bundle-workframe-ui.mjs +9 -0
- package/scripts/sync-canonical-to-package.mjs +9 -0
- package/scripts/verify-public-deploy.sh +121 -3
- package/workframe-api/action_proxy.py +23 -17
- package/workframe-api/activity_feed.py +798 -0
- package/workframe-api/api_meta.py +157 -0
- package/workframe-api/auth_gate.py +566 -0
- package/workframe-api/avatar_registry.py +343 -0
- package/workframe-api/broker_audit.py +164 -0
- package/workframe-api/cell_authority.py +231 -0
- package/workframe-api/chat_bind.py +319 -0
- package/workframe-api/chat_sessions.py +509 -0
- package/workframe-api/chat_stream.py +694 -0
- package/workframe-api/cleanup_dogfood_smoke.py +252 -0
- package/workframe-api/credential_broker.py +162 -0
- package/workframe-api/credential_resolve.py +202 -0
- package/workframe-api/credential_store.py +245 -0
- package/workframe-api/crew_registry.py +250 -0
- package/workframe-api/db_schema.py +755 -0
- package/workframe-api/docker_gateway.py +166 -0
- package/workframe-api/doctor_runtime.py +109 -0
- package/workframe-api/domain/__init__.py +47 -0
- package/workframe-api/domain/entities.py +252 -0
- package/workframe-api/domain/schema/workframe-domain.schema.json +278 -0
- package/workframe-api/egress_policy.py +42 -0
- package/workframe-api/handler_modules/__init__.py +17 -0
- package/workframe-api/handler_modules/handler_admin.py +542 -0
- package/workframe-api/handler_modules/handler_auth.py +512 -0
- package/workframe-api/handler_modules/handler_chat.py +641 -0
- package/workframe-api/handler_modules/handler_install.py +178 -0
- package/workframe-api/handler_modules/handler_provider.py +325 -0
- package/workframe-api/handler_modules/handler_workspace.py +1420 -0
- package/workframe-api/health_monitor.py +80 -0
- package/workframe-api/hermes_admin.py +756 -0
- package/workframe-api/hermes_profiles.py +1328 -0
- package/workframe-api/install_api.py +123 -0
- package/workframe-api/kanban_cron.py +473 -0
- package/workframe-api/lane_bindings.py +423 -0
- package/workframe-api/llm_proxy.py +17 -43
- package/workframe-api/mention_helpers.py +180 -0
- package/workframe-api/mention_invoke.py +431 -0
- package/workframe-api/model_surface.py +1139 -0
- package/workframe-api/oauth_pending.py +85 -0
- package/workframe-api/oauth_redirect.py +381 -0
- package/workframe-api/openrouter_catalog.py +4 -4
- package/workframe-api/package.json +2 -2
- package/workframe-api/profile_api_lifecycle.py +66 -0
- package/workframe-api/profile_config_yaml.py +126 -0
- package/workframe-api/profile_gateway.py +436 -0
- package/workframe-api/provider_bindings.py +673 -0
- package/workframe-api/provider_bootstrap.py +347 -0
- package/workframe-api/provider_catalog.py +180 -0
- package/workframe-api/rooms.py +1613 -0
- package/workframe-api/route_registry.py +470 -0
- package/workframe-api/run-typecheck.mjs +26 -0
- package/workframe-api/run_authority.py +287 -0
- package/workframe-api/run_ledger.py +470 -0
- package/workframe-api/run_surface_wiring.py +344 -0
- package/workframe-api/runtime_cohort.py +752 -0
- package/workframe-api/runtime_tokens.py +127 -0
- package/workframe-api/server.py +1376 -19305
- package/workframe-api/snapshot_feed.py +49 -0
- package/workframe-api/stack_config.py +33 -1
- package/workframe-api/supervisor_client.py +154 -0
- package/workframe-api/test_api_meta_build_stamp.py +34 -0
- package/workframe-api/test_cell_authority.py +79 -0
- package/workframe-api/test_chat_bind.py +48 -0
- package/workframe-api/test_credential_lease_matrix.py +171 -0
- package/workframe-api/test_credential_resolve.py +51 -0
- package/workframe-api/test_credential_store.py +27 -0
- package/workframe-api/test_dogfood_flows.py +346 -0
- package/workframe-api/test_domain_entities.py +152 -0
- package/workframe-api/test_exception_hygiene.py +12 -0
- package/workframe-api/test_hermes_admin.py +72 -0
- package/workframe-api/test_install_wizard_resume.py +78 -0
- package/workframe-api/test_local_bootstrap.py +67 -0
- package/workframe-api/test_mention_helpers.py +35 -0
- package/workframe-api/test_mention_invoke.py +26 -0
- package/workframe-api/test_model_surface_consistency.py +117 -0
- package/workframe-api/test_oauth_pending.py +41 -0
- package/workframe-api/test_profile_model_yaml.py +99 -0
- package/workframe-api/test_provider_bindings.py +52 -0
- package/workframe-api/test_provider_catalog.py +28 -0
- package/workframe-api/test_public_routes.py +41 -0
- package/workframe-api/test_route_registry.py +245 -0
- package/workframe-api/test_run_authority.py +112 -0
- package/workframe-api/test_run_ledger.py +157 -0
- package/workframe-api/test_run_surface_wiring.py +130 -0
- package/workframe-api/test_runtime_cohort.py +85 -0
- package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
- package/workframe-api/test_server_reexports.py +99 -0
- package/workframe-api/test_stack_config_install_smtp.py +7 -0
- package/workframe-api/time-bind-steps.py +75 -0
- package/workframe-api/time-perf.py +81 -0
- package/workframe-api/time-warm.py +107 -0
- package/workframe-api/turn_credentials.py +28 -13
- package/workframe-api/turn_overlay.py +715 -0
- package/workframe-api/updates.py +16 -1
- package/workframe-api/user_prefs.py +387 -0
- package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
- package/workframe-api/workspace_bootstrap.py +536 -0
- package/workframe-api/workspace_files.py +344 -0
- package/workframe-api/workspace_messaging.py +206 -0
- package/workframe-api/zk_auth.py +3 -2
- package/workframe-supervisor/server.py +10 -1
- package/workframe-supervisor/test_supervisor_negative.py +75 -0
- package/workframe-ui/public/assets/{arc-DjpS9a2b.js → arc-aUUffclm.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
- package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
- package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-mQWmB3z6.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
- package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
- package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
- package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-BTCEHb7l.js} +1 -1
- package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-CkbydpnK.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-DvrtArpk.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-yj_1x_qj.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
- package/workframe-ui/public/assets/{src-CY_By-r7.js → src-D6mvlP96.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
- package/workframe-ui/public/index.html +7 -6
- package/workframe-ui/public/workframe-build.json +5 -0
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
- package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-CsRdBqUB.js +0 -2
- package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
- package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
- package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.js +0 -1
package/README.md
CHANGED
package/bin/create-workframe.js
CHANGED
|
@@ -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}
|
|
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; --
|
|
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]
|
|
418
|
+
workframe doctor [--repair] [--json] Diagnose stack; --json mutation-free report
|
|
291
419
|
workframe setup Open Hermes setup (credentials)
|
|
292
420
|
workframe start Start the full stack (docker compose up -d)
|
|
293
421
|
workframe stop Stop all stack containers
|
package/docs/security.md
ADDED
|
@@ -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
|
@@ -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
|
|
96
|
-
gw_env="$(docker inspect
|
|
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 "
|
|
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
|
|
13
|
-
import
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
66
|
+
auth.status,
|
|
59
67
|
{"Content-Type": "application/json"},
|
|
60
|
-
|
|
68
|
+
credential_broker.broker_error_body(auth),
|
|
61
69
|
)
|
|
62
70
|
|
|
63
|
-
|
|
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 =
|
|
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")
|