create-workframe 0.1.17 → 0.1.20
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 +73 -9
- package/package.json +1 -1
- package/scripts/apply-update-workframe.sh +21 -18
- package/scripts/compose-docker-host.sh +26 -0
- package/workframe-api/action_proxy.py +8 -0
- package/workframe-api/chat_sessions.py +37 -6
- package/workframe-api/chat_stream.py +16 -0
- package/workframe-api/db_schema.py +29 -0
- package/workframe-api/handler_modules/handler_admin.py +32 -0
- package/workframe-api/handler_modules/handler_auth.py +6 -4
- package/workframe-api/handler_modules/handler_chat.py +17 -26
- package/workframe-api/handler_modules/handler_provider.py +13 -14
- package/workframe-api/handler_modules/handler_workspace.py +60 -2
- package/workframe-api/hermes_profiles.py +31 -9
- package/workframe-api/llm_proxy.py +18 -0
- package/workframe-api/mention_invoke.py +29 -24
- package/workframe-api/message_reactions.py +139 -0
- package/workframe-api/model_surface.py +329 -113
- package/workframe-api/package.json +4 -4
- package/workframe-api/profile_gateway.py +14 -6
- package/workframe-api/provider_bindings.py +118 -17
- package/workframe-api/provider_bootstrap.py +74 -38
- package/workframe-api/provider_model_catalog.py +261 -0
- package/workframe-api/rooms.py +4 -3
- package/workframe-api/route_registry.py +4 -0
- package/workframe-api/runtime_cohort.py +118 -23
- package/workframe-api/scripts/run-python.mjs +28 -0
- package/workframe-api/server.py +44 -12
- package/workframe-api/test_agent_identity_authority.py +96 -0
- package/workframe-api/test_chat_attribution_history.py +59 -0
- package/workframe-api/test_enrich_room_messages.py +43 -2
- package/workframe-api/test_llm_proxy_encoding.py +33 -0
- package/workframe-api/test_message_reactions.py +151 -0
- package/workframe-api/test_model_routing_reconcile.py +122 -0
- package/workframe-api/test_model_surface_consistency.py +45 -5
- package/workframe-api/test_oauth_llm.py +1 -1
- package/workframe-api/test_profile_gateway_health.py +37 -0
- package/workframe-api/test_provider_bindings.py +115 -1
- package/workframe-api/test_provider_model_catalog.py +27 -0
- package/workframe-api/test_provider_model_surface.py +2 -2
- package/workframe-api/test_runtime_lease_lifecycle.py +93 -0
- package/workframe-api/test_server_reexports.py +2 -0
- package/workframe-api/test_user_llm_prefs.py +27 -0
- package/workframe-api/test_workspace_file_actions.py +119 -0
- package/workframe-api/turn_credentials.py +41 -0
- package/workframe-api/turn_overlay.py +95 -34
- package/workframe-api/updates.py +14 -5
- package/workframe-api/user_prefs.py +15 -0
- package/workframe-api/workspace_bootstrap.py +1 -0
- package/workframe-api/workspace_files.py +124 -0
- package/workframe-supervisor/server.py +18 -2
- package/workframe-ui/public/assets/{arc-esSBm9C_.js → arc-BkATcoqt.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-yusDFKmK.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DVVz9RGn.js → architectureDiagram-3BPJPVTR-31_yAiUS.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-B6i10GMX.js → blockDiagram-GPEHLZMM-ClM6Si9T.js} +1 -1
- package/workframe-ui/public/assets/{brave-B885JYnH.svg → brave-BoYMbZi1.svg} +0 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BqwJ7JoH.js → c4Diagram-AAUBKEIU-DJzEktF3.js} +1 -1
- package/workframe-ui/public/assets/channel-Nb_UjZ3O.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-CyhcedSa.js → chunk-2J33WTMH-DPFiyb5O.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-1Rr6WuvI.js → chunk-3OPIFGDE-DXNJzuCn.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-D2bRrARV.js → chunk-4BX2VUAB-DDNibGdr.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-CKXeZxSr.js → chunk-55IACEB6-Co3_ulwA.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DYQxxPZ4.js → chunk-5ZQYHXKU-Cmxz8o4d.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-CGpIgqvD.js → chunk-727SXJPM-CDAQGesU.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-B88NGQ7d.js → chunk-AQP2D5EJ-x-swy3kc.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-CCymiI0O.js → chunk-BSJP7CBP-28nU0JOE.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-DRgey03j.js → chunk-CSCIHK7Q-FUURFika.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-l1adn8gM.js → chunk-FMBD7UC4-B4RgRO2R.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-CaEvDRPK.js → chunk-KSCS5N6A-CkuppW3T.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-BgCnvPTv.js → chunk-L5ZTLDWV-DyrPSaz3.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-t2zokHpn.js → chunk-LZXEDZCA-DFaZSDNV.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-C3c9p0sv.js → chunk-ND2GUHAM-DXI0OJxg.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-DSujF-R8.js → chunk-NZK2D7GU-CyOp1ZG2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-DJJTbM6p.js → chunk-O5CBEL6O-tZZMCfbx.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C_TtOXi5.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-BjSJQEIY.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-D2Cxn3AG.js → chunk-XPW4576I-B7_2gS3x.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-JD6iZUGh.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-JD6iZUGh.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A---dQ-pFK.js → cose-bilkent-S5V4N54A-BWJaHw_L.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-B_qe1ODY.js → dagre-BM42HDAG-Bu5MadGV.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Bou8vEYl.js → diagram-2AECGRRQ-ckLZkDFD.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-DEUEzONU.js → diagram-5GNKFQAL-Cf6Abx1Y.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Ctq571VA.js → diagram-KO2AKTUF-TLk7_5mJ.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CnKn89lm.js → diagram-LMA3HP47-ouNxsAb0.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-DnKlrMx2.js → diagram-OG6HWLK6-BMCBQ35D.js} +1 -1
- package/workframe-ui/public/assets/{dist-ubrEgQyZ.js → dist-C9bxMexz.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-PjqO7CJd.js → erDiagram-TEJ5UH35-BN3e6jb1.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Bqd_sTz0.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-B2PN_nNG.js → flowDiagram-I6XJVG4X-BiinzZrd.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-ThyAtNQP.js → ganttDiagram-6RSMTGT7-AYE8oBBG.js} +1 -1
- package/workframe-ui/public/assets/{gemini-Cq-0-rmr.svg → gemini-DqnZlvMo.svg} +0 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-CB2UgoWy.js → gitGraph-WXDBUCRP-mIq_E2vs.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-DRJJ-zT-.js → gitGraphDiagram-PVQCEYII-Cf8IFEcO.js} +1 -1
- package/workframe-ui/public/assets/index-BnfFRGR3.js +133 -0
- package/workframe-ui/public/assets/index-COshPjF3.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-CLtR33Q_.js → info-J43DQDTF--_3kCaIQ.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-Cmunqfvi.js → infoDiagram-5YYISTIA-BuIOyYMu.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-DWE_84eL.js → ishikawaDiagram-YF4QCWOH-D3bbsXQ8.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-BtBhpyRL.js → journeyDiagram-JHISSGLW-BrZ26C8L.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-B8QspDDL.js → kanban-definition-UN3LZRKU-CJiUyUOr.js} +1 -1
- package/workframe-ui/public/assets/{line-iOgDn4O8.js → line-Bee1snmU.js} +1 -1
- package/workframe-ui/public/assets/{linear-BBehZNVA.js → linear-dfD-zlI7.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-DQK5HcdU.js → mermaid-parser.core-D6IqqDSH.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-Dsrdd1Wr.js → mermaid.core-CuuxYt2r.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-Ds_BfsJA.js → mindmap-definition-RKZ34NQL-T7W2fkwA.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-Bz3pi4i8.js → packet-YPE3B663-CiXMDasj.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-BFFah-6g.js → pie-LRSECV5Y-CUx1_96f.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CVCvLPcC.js → pieDiagram-4H26LBE5-DfR96FUO.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-Cx6rrc_7.js → quadrantDiagram-W4KKPZXB-Dj6mqgQE.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-DJ5vN_cg.js → radar-GUYGQ44K-DKT2q98y.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-Cbry6b5c.js → requirementDiagram-4Y6WPE33-DYDnBVw9.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-CICUr5En.js → sankeyDiagram-5OEKKPKP-BSkxpjjO.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BsOuB3F-.js → sequenceDiagram-3UESZ5HK-Dz6SNH9S.js} +1 -1
- package/workframe-ui/public/assets/{src-TS8jZspq.js → src-DC521nE_.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-avBzMQy_.js → stateDiagram-AJRCARHV-D4ZoF5NP.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DknveEiR.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DHLm6UFN.js → timeline-definition-PNZ67QCA-d0ezg1Hu.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-B3Qd5t3j.js → treeView-BLDUP644-kXyaqwIy.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-D1dMQS7x.js → treemap-LRROVOQU-g168p7-O.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-DZ6SoniY.js → vennDiagram-CIIHVFJN-C0_QsmRl.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-9gK17Tp3.js → wardley-L42UT6IY-BNFCdzxQ.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-FCU_wmRE.js → wardleyDiagram-YWT4CUSO-BOwUS9sY.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DWowvseN.js → xychartDiagram-2RQKCTM6-Cxr1ZiKh.js} +1 -1
- package/workframe-ui/public/index.html +2 -2
- package/workframe-ui/public/workframe-build.json +3 -3
- package/workframe-ui/public/assets/architecture-7EHR7CIX-j-DiBdGu.js +0 -1
- package/workframe-ui/public/assets/channel-CJ9pFXbx.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-Bxk3enxo.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DdbkX1rg.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-bwCRiiUl.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-bwCRiiUl.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-g-Xo_eS4.js +0 -1
- package/workframe-ui/public/assets/index-1euVfd3b.js +0 -132
- package/workframe-ui/public/assets/index-BLVAsAeO.css +0 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-BFRA5TYD.js +0 -1
package/README.md
CHANGED
package/bin/create-workframe.js
CHANGED
|
@@ -1360,10 +1360,27 @@ echo "Done. Mission control and workframe unchanged unless you recreate the full
|
|
|
1360
1360
|
`;
|
|
1361
1361
|
}
|
|
1362
1362
|
|
|
1363
|
-
function launchPhaseBInstaller(target) {
|
|
1363
|
+
function launchPhaseBInstaller(target, { wait = false, noBrowser = false } = {}) {
|
|
1364
1364
|
if (process.platform === 'win32') {
|
|
1365
1365
|
const script = path.join(target, 'scripts', 'start-install.ps1');
|
|
1366
1366
|
if (!fs.existsSync(script)) return false;
|
|
1367
|
+
if (wait) {
|
|
1368
|
+
const args = [
|
|
1369
|
+
'-NoProfile',
|
|
1370
|
+
'-ExecutionPolicy',
|
|
1371
|
+
'Bypass',
|
|
1372
|
+
'-File',
|
|
1373
|
+
script,
|
|
1374
|
+
'-NoPrompt',
|
|
1375
|
+
];
|
|
1376
|
+
if (noBrowser) args.push('-NoBrowser');
|
|
1377
|
+
const child = spawnSync('powershell.exe', args, { cwd: target, stdio: 'inherit' });
|
|
1378
|
+
if (child.error) throw child.error;
|
|
1379
|
+
if (child.status !== 0) {
|
|
1380
|
+
throw new Error(`Phase B installer failed (${child.status ?? 'unknown exit'})`);
|
|
1381
|
+
}
|
|
1382
|
+
return true;
|
|
1383
|
+
}
|
|
1367
1384
|
const escaped = script.replace(/'/g, "''");
|
|
1368
1385
|
const child = spawn(
|
|
1369
1386
|
'powershell.exe',
|
|
@@ -1386,6 +1403,16 @@ function launchPhaseBInstaller(target) {
|
|
|
1386
1403
|
}
|
|
1387
1404
|
const script = path.join(target, 'scripts', 'start-install.sh');
|
|
1388
1405
|
if (!fs.existsSync(script)) return false;
|
|
1406
|
+
if (wait) {
|
|
1407
|
+
const args = ['--no-prompt'];
|
|
1408
|
+
if (noBrowser) args.push('--no-browser');
|
|
1409
|
+
const child = spawnSync(script, args, { cwd: target, stdio: 'inherit' });
|
|
1410
|
+
if (child.error) throw child.error;
|
|
1411
|
+
if (child.status !== 0) {
|
|
1412
|
+
throw new Error(`Phase B installer failed (${child.status ?? 'unknown exit'})`);
|
|
1413
|
+
}
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1389
1416
|
const child = spawn(script, [], {
|
|
1390
1417
|
cwd: target,
|
|
1391
1418
|
detached: true,
|
|
@@ -1520,7 +1547,12 @@ ${openWorkframeBrowserBlockSh()}
|
|
|
1520
1547
|
}
|
|
1521
1548
|
|
|
1522
1549
|
function startInstallPs1(docker, nativeProfile, nativeAgentName, ports) {
|
|
1523
|
-
return
|
|
1550
|
+
return `param(
|
|
1551
|
+
[switch]$NoBrowser,
|
|
1552
|
+
[switch]$NoPrompt
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
$ErrorActionPreference = 'Stop'
|
|
1524
1556
|
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
|
1525
1557
|
Set-Location $Root
|
|
1526
1558
|
|
|
@@ -1533,7 +1565,9 @@ Write-Host " Project: ${nativeAgentName}" -ForegroundColor Cyan
|
|
|
1533
1565
|
Write-Host '========================================' -ForegroundColor Cyan
|
|
1534
1566
|
Write-Host ''
|
|
1535
1567
|
|
|
1536
|
-
|
|
1568
|
+
if (-not $NoBrowser) {
|
|
1569
|
+
& "$Root\\scripts\\open-install-ui.ps1"
|
|
1570
|
+
}
|
|
1537
1571
|
|
|
1538
1572
|
Write-Host 'Step 1/2 - Starting Docker stack...' -ForegroundColor Yellow
|
|
1539
1573
|
${composeUpPs1Block()}
|
|
@@ -1558,8 +1592,14 @@ Write-Host ' Workframe UI: http://127.0.0.1:${ports.ui}'
|
|
|
1558
1592
|
Write-Host ' Hermes dashboard: http://127.0.0.1:${ports.dashboard}'
|
|
1559
1593
|
Write-Host ''
|
|
1560
1594
|
Write-Host 'LLM keys are configured in the onboarding UI - no Hermes TUI required.' -ForegroundColor DarkGray
|
|
1561
|
-
${
|
|
1562
|
-
|
|
1595
|
+
if (-not $NoBrowser) {
|
|
1596
|
+
Write-Host ''
|
|
1597
|
+
Write-Host 'Opening Workframe setup wizard (/install)...' -ForegroundColor Green
|
|
1598
|
+
& "$Root\\scripts\\open-install-ui.ps1"
|
|
1599
|
+
}
|
|
1600
|
+
if (-not $NoPrompt) {
|
|
1601
|
+
Read-Host 'Press Enter to close'
|
|
1602
|
+
}
|
|
1563
1603
|
`;
|
|
1564
1604
|
}
|
|
1565
1605
|
|
|
@@ -1569,6 +1609,15 @@ set -euo pipefail
|
|
|
1569
1609
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
1570
1610
|
cd "$ROOT"
|
|
1571
1611
|
|
|
1612
|
+
NO_BROWSER=0
|
|
1613
|
+
for arg in "$@"; do
|
|
1614
|
+
case "$arg" in
|
|
1615
|
+
--no-browser) NO_BROWSER=1 ;;
|
|
1616
|
+
--no-prompt) ;;
|
|
1617
|
+
*) echo "Unknown option: $arg" >&2; exit 2 ;;
|
|
1618
|
+
esac
|
|
1619
|
+
done
|
|
1620
|
+
|
|
1572
1621
|
mkdir -p Agents Files
|
|
1573
1622
|
|
|
1574
1623
|
echo ""
|
|
@@ -1578,7 +1627,9 @@ echo " Project: ${nativeAgentName}"
|
|
|
1578
1627
|
echo "========================================"
|
|
1579
1628
|
echo ""
|
|
1580
1629
|
|
|
1581
|
-
"$
|
|
1630
|
+
if [ "$NO_BROWSER" -eq 0 ]; then
|
|
1631
|
+
"$ROOT/scripts/open-install-ui.sh" || true
|
|
1632
|
+
fi
|
|
1582
1633
|
|
|
1583
1634
|
echo "Step 1/2 — Starting Docker stack..."
|
|
1584
1635
|
${composeUpBashBlock()}
|
|
@@ -1598,7 +1649,11 @@ echo " Workframe UI: http://127.0.0.1:${ports.ui}"
|
|
|
1598
1649
|
echo " Hermes dashboard: http://127.0.0.1:${ports.dashboard}/"
|
|
1599
1650
|
echo ""
|
|
1600
1651
|
echo "LLM keys are configured in the onboarding UI - no Hermes TUI required."
|
|
1601
|
-
$
|
|
1652
|
+
if [ "$NO_BROWSER" -eq 0 ]; then
|
|
1653
|
+
echo ""
|
|
1654
|
+
echo "Opening Workframe setup wizard (/install)..."
|
|
1655
|
+
"$ROOT/scripts/open-install-ui.sh" || true
|
|
1656
|
+
fi
|
|
1602
1657
|
`;
|
|
1603
1658
|
}
|
|
1604
1659
|
|
|
@@ -1827,6 +1882,8 @@ function parseArgs(argv) {
|
|
|
1827
1882
|
else if (a === '--run-docker-pull') args.runDockerPull = true;
|
|
1828
1883
|
else if (a === '--allow-install-actions') args.installGuideOnly = false;
|
|
1829
1884
|
else if (a === '--no-launch') args.noLaunch = true;
|
|
1885
|
+
else if (a === '--wait') args.waitForInstall = true;
|
|
1886
|
+
else if (a === '--no-browser') args.noBrowser = true;
|
|
1830
1887
|
else if (a === '--slot') args.slot = Number(argv[++i]);
|
|
1831
1888
|
else if (a.startsWith('--slot=')) args.slot = Number(a.split('=', 2)[1]);
|
|
1832
1889
|
else if (a === '--deploy') args.deploy = argv[++i];
|
|
@@ -1867,6 +1924,8 @@ Flags:
|
|
|
1867
1924
|
--allow-install-actions Allow installer to execute shell install actions
|
|
1868
1925
|
-y, --yes Non-interactive defaults
|
|
1869
1926
|
--no-launch Scaffold only; do not open Phase B installer
|
|
1927
|
+
--wait Run Phase B here and fail if installation fails
|
|
1928
|
+
--no-browser With --wait, do not open the system browser
|
|
1870
1929
|
`);
|
|
1871
1930
|
}
|
|
1872
1931
|
|
|
@@ -2799,9 +2858,14 @@ async function main() {
|
|
|
2799
2858
|
console.log(`Bootstrap default: bootstrap-native (full pack: ${profiles.length} profiles)`);
|
|
2800
2859
|
console.log(`SOUL seeds: ${seedProfiles.length} profiles + native SETUP playbook`);
|
|
2801
2860
|
if (!args.ci && !args.noLaunch) {
|
|
2802
|
-
if (launchPhaseBInstaller(target
|
|
2861
|
+
if (launchPhaseBInstaller(target, {
|
|
2862
|
+
wait: Boolean(args.waitForInstall),
|
|
2863
|
+
noBrowser: Boolean(args.noBrowser),
|
|
2864
|
+
})) {
|
|
2803
2865
|
console.log('');
|
|
2804
|
-
console.log(
|
|
2866
|
+
console.log(args.waitForInstall
|
|
2867
|
+
? 'Installer completed successfully.'
|
|
2868
|
+
: 'Launching installer — browser opens /install immediately.');
|
|
2805
2869
|
console.log(`Continue setup at http://127.0.0.1:${ports.ui}/install`);
|
|
2806
2870
|
}
|
|
2807
2871
|
} else if (args.noLaunch) {
|
package/package.json
CHANGED
|
@@ -6,15 +6,18 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
6
6
|
source "$SCRIPT_DIR/compose-docker-host.sh"
|
|
7
7
|
|
|
8
8
|
# ponytail: npm integrity via python3 — Alpine supervisor has no xxd
|
|
9
|
-
|
|
10
|
-
local tarball="$1"
|
|
9
|
+
_wf_prefetched_integrity_ok() {
|
|
10
|
+
local tarball="$1" manifest="${1}.sha512"
|
|
11
11
|
local expected actual
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
if [[ ! -s "$manifest" ]]; then
|
|
13
|
+
echo "prefetched tarball integrity manifest is missing: $manifest" >&2
|
|
14
|
+
return 1
|
|
15
|
+
fi
|
|
14
16
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
15
|
-
echo "
|
|
16
|
-
return
|
|
17
|
+
echo "python3 is required to verify the prefetched update tarball" >&2
|
|
18
|
+
return 1
|
|
17
19
|
fi
|
|
20
|
+
expected="$(tr -d '\r\n' < "$manifest")"
|
|
18
21
|
actual="$(python3 -c 'import base64, hashlib, sys
|
|
19
22
|
p = sys.argv[1]
|
|
20
23
|
d = hashlib.sha512(open(p, "rb").read()).digest()
|
|
@@ -69,11 +72,12 @@ _wf_sync_from_pack_dir() {
|
|
|
69
72
|
_wf_apply_npm_tarball() {
|
|
70
73
|
local tarball="$1"
|
|
71
74
|
local ver="${2:-latest}"
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
local source="${3:-npm}"
|
|
76
|
+
if [[ "$source" == "prefetched" ]] && ! _wf_prefetched_integrity_ok "$tarball"; then
|
|
77
|
+
echo "integrity mismatch for API-prefetched ${NPM_PACKAGE}@${ver}" >&2
|
|
74
78
|
exit 1
|
|
75
79
|
fi
|
|
76
|
-
echo "integrity
|
|
80
|
+
echo "integrity verified: ${NPM_PACKAGE}@${ver} ($source)"
|
|
77
81
|
local extract_dir
|
|
78
82
|
extract_dir="$(mktemp -d)"
|
|
79
83
|
tar -xf "$tarball" -C "$extract_dir"
|
|
@@ -125,10 +129,9 @@ if [[ "${WF_APPLY_UPDATE_SELF_CHECK:-}" == "1" ]]; then
|
|
|
125
129
|
TMP="$(mktemp -d)"
|
|
126
130
|
trap 'rm -rf "$TMP"' EXIT
|
|
127
131
|
echo test > "$TMP/t"
|
|
128
|
-
if ! _wf_npm_integrity_ok "$TMP/t" create-workframe latest 2>/dev/null; then
|
|
129
|
-
: # empty file won't match registry integrity — exercise python path only
|
|
130
|
-
fi
|
|
131
132
|
command -v python3 >/dev/null || { echo "self-check: python3 required" >&2; exit 1; }
|
|
133
|
+
python3 -c 'import base64, hashlib, sys; print("sha512-" + base64.b64encode(hashlib.sha512(open(sys.argv[1], "rb").read()).digest()).decode())' "$TMP/t" > "$TMP/t.sha512"
|
|
134
|
+
_wf_prefetched_integrity_ok "$TMP/t"
|
|
132
135
|
echo "apply-update-workframe self-check ok"
|
|
133
136
|
exit 0
|
|
134
137
|
fi
|
|
@@ -147,7 +150,7 @@ PREFETCH_TARBALL="${WORKFRAME_UPDATE_TARBALL:-}"
|
|
|
147
150
|
|
|
148
151
|
if [[ -n "$PREFETCH_TARBALL" && -f "$PREFETCH_TARBALL" ]]; then
|
|
149
152
|
echo "Applying API-prefetched tarball: $PREFETCH_TARBALL"
|
|
150
|
-
_wf_apply_npm_tarball "$PREFETCH_TARBALL" "${TARGET_VERSION:-latest}"
|
|
153
|
+
_wf_apply_npm_tarball "$PREFETCH_TARBALL" "${TARGET_VERSION:-latest}" prefetched
|
|
151
154
|
elif [[ "${WORKFRAME_UPDATE_SKIP_NPM:-1}" == "1" ]] && [[ "${WORKFRAME_UPDATE_ALLOW_NPM:-}" != "1" ]]; then
|
|
152
155
|
echo "Skipping npm template sync (WORKFRAME_UPDATE_SKIP_NPM=1; set WORKFRAME_UPDATE_ALLOW_NPM=1 to fetch)"
|
|
153
156
|
elif command -v npm >/dev/null 2>&1; then
|
|
@@ -164,7 +167,7 @@ elif command -v npm >/dev/null 2>&1; then
|
|
|
164
167
|
fi
|
|
165
168
|
TARBALL="$(ls -1 "$TMP"/${NPM_PACKAGE}-*.tgz 2>/dev/null | head -n1 || true)"
|
|
166
169
|
if [[ -n "$TARBALL" ]]; then
|
|
167
|
-
_wf_apply_npm_tarball "$TARBALL" "${TARGET_VERSION:-latest}"
|
|
170
|
+
_wf_apply_npm_tarball "$TARBALL" "${TARGET_VERSION:-latest}" npm
|
|
168
171
|
else
|
|
169
172
|
echo "npm pack produced no tarball — skipping template sync" >&2
|
|
170
173
|
exit 1
|
|
@@ -177,19 +180,19 @@ echo "Rebuilding workframe-api and workframe-supervisor..."
|
|
|
177
180
|
workframe_compose build workframe-api workframe-supervisor
|
|
178
181
|
if [[ "${WORKFRAME_UPDATE_FROM_SUPERVISOR:-}" == "1" ]]; then
|
|
179
182
|
# ponytail: supervisor cannot recreate itself inside the stack.apply request — defer after handler returns
|
|
180
|
-
|
|
183
|
+
workframe_compose_host_bindings up -d --no-build --no-deps workframe-api
|
|
181
184
|
(
|
|
182
185
|
sleep 3
|
|
183
|
-
|
|
186
|
+
workframe_compose_host_bindings up -d --no-build --no-deps workframe-supervisor
|
|
184
187
|
) >/tmp/workframe-supervisor-restart.log 2>&1 &
|
|
185
188
|
else
|
|
186
189
|
workframe_compose up -d --build --no-deps workframe-api workframe-supervisor
|
|
187
190
|
fi
|
|
188
191
|
|
|
189
192
|
if workframe_compose config --services 2>/dev/null | grep -qx workframe-ui; then
|
|
190
|
-
|
|
193
|
+
workframe_compose_host_bindings up -d --no-build --no-deps workframe-ui || workframe_compose_host_bindings restart workframe-ui || true
|
|
191
194
|
elif workframe_compose config --services 2>/dev/null | grep -qx workframe; then
|
|
192
|
-
|
|
195
|
+
workframe_compose_host_bindings up -d --no-build --no-deps workframe || workframe_compose_host_bindings restart workframe || true
|
|
193
196
|
fi
|
|
194
197
|
|
|
195
198
|
_wf_record_package_version "${TARGET_VERSION:-}"
|
|
@@ -6,6 +6,17 @@ workframe_compose_prepare() {
|
|
|
6
6
|
compose_cd=""
|
|
7
7
|
compose_files=()
|
|
8
8
|
|
|
9
|
+
# A supervisor invokes compose through the Docker socket. Its /compose mount is
|
|
10
|
+
# the only build context it can package, so never feed the host-bindings overlay
|
|
11
|
+
# back into that invocation (Windows paths become /compose/D:/... in build).
|
|
12
|
+
if [[ "${WORKFRAME_UPDATE_FROM_SUPERVISOR:-}" == "1" \
|
|
13
|
+
&& -n "${WORKFRAME_COMPOSE_DIR:-}" \
|
|
14
|
+
&& -f "${WORKFRAME_COMPOSE_DIR}/docker-compose.yml" ]]; then
|
|
15
|
+
compose_cd="${WORKFRAME_COMPOSE_DIR}"
|
|
16
|
+
compose_files=(-f docker-compose.yml)
|
|
17
|
+
return 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
9
20
|
# Host-bindings overlay: absolute WORKFRAME_HOST_* paths for docker.sock on the real host.
|
|
10
21
|
# Skip inside supervisor when only the /compose bind mount is visible — host paths become
|
|
11
22
|
# /compose/D:/... and break build contexts.
|
|
@@ -43,3 +54,18 @@ workframe_compose() {
|
|
|
43
54
|
fi
|
|
44
55
|
docker compose "${compose_files[@]}" "$@"
|
|
45
56
|
}
|
|
57
|
+
|
|
58
|
+
# Builds from inside the supervisor use /compose, but recreating a service must
|
|
59
|
+
# pass the generated host-bindings overlay back to Docker Desktop for its binds.
|
|
60
|
+
workframe_compose_host_bindings() {
|
|
61
|
+
if [[ "${WORKFRAME_UPDATE_FROM_SUPERVISOR:-}" == "1" \
|
|
62
|
+
&& -n "${WORKFRAME_HOST_COMPOSE_DIR:-}" \
|
|
63
|
+
&& -n "${WORKFRAME_HOST_PROJECT_ROOT:-}" \
|
|
64
|
+
&& -n "${WORKFRAME_COMPOSE_DIR:-}" \
|
|
65
|
+
&& -f "${WORKFRAME_COMPOSE_DIR}/docker-compose.host-bindings.yml" ]]; then
|
|
66
|
+
cd "${WORKFRAME_COMPOSE_DIR}"
|
|
67
|
+
docker compose -f docker-compose.yml -f docker-compose.host-bindings.yml "$@"
|
|
68
|
+
return
|
|
69
|
+
fi
|
|
70
|
+
workframe_compose "$@"
|
|
71
|
+
}
|
|
@@ -82,8 +82,10 @@ def forward_request(
|
|
|
82
82
|
"content-length",
|
|
83
83
|
"authorization",
|
|
84
84
|
"x-api-key",
|
|
85
|
+
"accept-encoding",
|
|
85
86
|
}
|
|
86
87
|
}
|
|
88
|
+
upstream_headers["Accept-Encoding"] = "identity"
|
|
87
89
|
upstream_headers.update(upstream_auth_header(provider, secret))
|
|
88
90
|
|
|
89
91
|
req = urllib.request.Request(url, data=body, headers=upstream_headers, method=method.upper())
|
|
@@ -91,10 +93,16 @@ def forward_request(
|
|
|
91
93
|
with urllib.request.urlopen(req, timeout=600) as resp:
|
|
92
94
|
resp_body = resp.read()
|
|
93
95
|
out_headers = {"Content-Type": resp.headers.get("Content-Type", "application/octet-stream")}
|
|
96
|
+
content_encoding = resp.headers.get("Content-Encoding", "").strip()
|
|
97
|
+
if content_encoding:
|
|
98
|
+
out_headers["Content-Encoding"] = content_encoding
|
|
94
99
|
return resp.status, out_headers, resp_body
|
|
95
100
|
except urllib.error.HTTPError as exc:
|
|
96
101
|
raw = exc.read()
|
|
97
102
|
out_headers = {"Content-Type": exc.headers.get("Content-Type", "application/json")}
|
|
103
|
+
content_encoding = exc.headers.get("Content-Encoding", "").strip()
|
|
104
|
+
if content_encoding:
|
|
105
|
+
out_headers["Content-Encoding"] = content_encoding
|
|
98
106
|
return exc.code, out_headers, raw
|
|
99
107
|
|
|
100
108
|
|
|
@@ -213,6 +213,34 @@ def _llm_attribution_for_profile(profile: str, session_model: str = "") -> tuple
|
|
|
213
213
|
return model, str(billing or "").strip()
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def _session_run_attributions(profile: str, session_id: str) -> list[tuple[str, str]]:
|
|
217
|
+
"""Completed-turn model/provider pairs in the same order as chat turns."""
|
|
218
|
+
prof = str(profile or "").strip()
|
|
219
|
+
sid = str(session_id or "").strip()
|
|
220
|
+
if not prof or not sid:
|
|
221
|
+
return []
|
|
222
|
+
conn = _srv()._workframe_db()
|
|
223
|
+
try:
|
|
224
|
+
rows = conn.execute(
|
|
225
|
+
"""
|
|
226
|
+
SELECT li.model, li.provider
|
|
227
|
+
FROM runs r
|
|
228
|
+
JOIN run_line_items li ON li.run_id = r.run_id AND li.kind = 'llm_turn'
|
|
229
|
+
WHERE r.session_id = ? AND r.profile_slug = ? AND r.status = 'completed'
|
|
230
|
+
ORDER BY r.created_at ASC, li.created_at ASC, li.line_item_id ASC
|
|
231
|
+
""",
|
|
232
|
+
(sid, prof),
|
|
233
|
+
).fetchall()
|
|
234
|
+
except sqlite3.Error:
|
|
235
|
+
return []
|
|
236
|
+
finally:
|
|
237
|
+
conn.close()
|
|
238
|
+
return [
|
|
239
|
+
(str(row["model"] or "").strip(), str(row["provider"] or "").strip())
|
|
240
|
+
for row in rows
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
|
|
216
244
|
def _latest_session_id(profile: str) -> str:
|
|
217
245
|
"""Most recent session — prefer active (ended_at IS NULL), then by started_at."""
|
|
218
246
|
db = _srv()._profile_dir(profile) / "state.db"
|
|
@@ -441,7 +469,8 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
|
|
|
441
469
|
_srv()._runtime_template_slug(profile) if _srv()._is_runtime_profile_slug(profile) else profile
|
|
442
470
|
)
|
|
443
471
|
display = _srv()._profile_display_name(profile)
|
|
444
|
-
|
|
472
|
+
run_attributions = _session_run_attributions(profile, sid)
|
|
473
|
+
agent_turn_index = 0
|
|
445
474
|
try:
|
|
446
475
|
raw = conn.execute(
|
|
447
476
|
"""
|
|
@@ -483,12 +512,14 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
|
|
|
483
512
|
if current and current["role"] == "agent":
|
|
484
513
|
current["segments"].extend(segments)
|
|
485
514
|
current["tokens"] += tokens
|
|
486
|
-
if attr_model:
|
|
487
|
-
current["model"] = attr_model
|
|
488
|
-
current["llm_provider"] = attr_provider
|
|
489
515
|
else:
|
|
490
516
|
if current:
|
|
491
517
|
turns.append(current)
|
|
518
|
+
if agent_turn_index < len(run_attributions):
|
|
519
|
+
attr_model, attr_provider = run_attributions[agent_turn_index]
|
|
520
|
+
else:
|
|
521
|
+
attr_model, attr_provider = "", ""
|
|
522
|
+
agent_turn_index += 1
|
|
492
523
|
current = {
|
|
493
524
|
"id": f"turn-{m['id']}",
|
|
494
525
|
"authorId": author_profile,
|
|
@@ -497,8 +528,8 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
|
|
|
497
528
|
"segments": list(segments),
|
|
498
529
|
"timestamp": ts,
|
|
499
530
|
"tokens": tokens,
|
|
500
|
-
"model": attr_model,
|
|
501
|
-
"llm_provider": attr_provider,
|
|
531
|
+
**({"model": attr_model} if attr_model else {}),
|
|
532
|
+
**({"llm_provider": attr_provider} if attr_provider else {}),
|
|
502
533
|
}
|
|
503
534
|
if current:
|
|
504
535
|
turns.append(current)
|
|
@@ -391,6 +391,22 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
391
391
|
raise ValueError("session_id required")
|
|
392
392
|
if not text:
|
|
393
393
|
raise ValueError("text required")
|
|
394
|
+
# Bootstrap before reading the model block. Existing generated profiles may
|
|
395
|
+
# need provider/model migrations; reading first dispatches one stale turn.
|
|
396
|
+
bootstrapped = _srv()._bootstrap_profile_providers(prof, _triggering_user, _workspace_id)
|
|
397
|
+
# Native/template profiles can still contain the installer default
|
|
398
|
+
# (provider=openrouter + public base_url). A user vault credential must
|
|
399
|
+
# always be consumed through Workframe's authenticated proxy.
|
|
400
|
+
if _triggering_user:
|
|
401
|
+
reconciled = _srv()._reconcile_profile_llm_for_user(
|
|
402
|
+
prof, _triggering_user, _workspace_id,
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
reconciled = False
|
|
406
|
+
if bootstrapped and not reconciled:
|
|
407
|
+
# Hermes caches config.yaml. Reload before dispatching so the first
|
|
408
|
+
# turn after a repair or model save cannot use stale routing.
|
|
409
|
+
_srv()._reload_runtime_profile_gateway(prof, wait_healthy=True)
|
|
394
410
|
model_block = _srv()._read_model_block(prof)
|
|
395
411
|
llm_provider = _srv()._llm_billing_provider(
|
|
396
412
|
prof, user_id=_triggering_user, workspace_id=_workspace_id, block=model_block,
|
|
@@ -588,6 +588,33 @@ def _migrate_v13_workspace_members_in_spaces(conn: sqlite3.Connection) -> None:
|
|
|
588
588
|
VALUES ('13', 'workspace members joined to all space rooms', datetime('now'))
|
|
589
589
|
""",
|
|
590
590
|
)
|
|
591
|
+
def _migrate_v15_message_reactions(conn: sqlite3.Connection) -> None:
|
|
592
|
+
if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '15' LIMIT 1").fetchone():
|
|
593
|
+
return
|
|
594
|
+
conn.execute(
|
|
595
|
+
"""
|
|
596
|
+
CREATE TABLE IF NOT EXISTS message_reactions (
|
|
597
|
+
scope_key TEXT NOT NULL,
|
|
598
|
+
message_id TEXT NOT NULL,
|
|
599
|
+
user_id TEXT NOT NULL,
|
|
600
|
+
emoji TEXT NOT NULL,
|
|
601
|
+
created_at TEXT NOT NULL,
|
|
602
|
+
PRIMARY KEY (scope_key, message_id, user_id, emoji)
|
|
603
|
+
)
|
|
604
|
+
"""
|
|
605
|
+
)
|
|
606
|
+
conn.execute(
|
|
607
|
+
"""
|
|
608
|
+
CREATE INDEX IF NOT EXISTS idx_message_reactions_scope_message
|
|
609
|
+
ON message_reactions(scope_key, message_id, created_at)
|
|
610
|
+
"""
|
|
611
|
+
)
|
|
612
|
+
conn.execute(
|
|
613
|
+
"""
|
|
614
|
+
INSERT INTO schema_migrations (version, description, applied_at)
|
|
615
|
+
VALUES ('15', 'message reactions for room and agent-DM chat surfaces', datetime('now'))
|
|
616
|
+
""",
|
|
617
|
+
)
|
|
591
618
|
def _migrate_v8_room_avatars(conn: sqlite3.Connection) -> None:
|
|
592
619
|
if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '8' LIMIT 1").fetchone():
|
|
593
620
|
return
|
|
@@ -738,6 +765,7 @@ def ensure_workframe_db_schema(open_db: OpenDb) -> None:
|
|
|
738
765
|
_migrate_v11_delegation_kanban_boards(conn)
|
|
739
766
|
_migrate_v12_adopt_install_keys_to_owners(conn)
|
|
740
767
|
_migrate_v13_workspace_members_in_spaces(conn)
|
|
768
|
+
_migrate_v15_message_reactions(conn)
|
|
741
769
|
conn.commit()
|
|
742
770
|
finally:
|
|
743
771
|
conn.close()
|
|
@@ -750,6 +778,7 @@ def ensure_oauth_pending_migrations(open_db: OpenDb) -> None:
|
|
|
750
778
|
_migrate_v11_delegation_kanban_boards(conn)
|
|
751
779
|
_migrate_v12_adopt_install_keys_to_owners(conn)
|
|
752
780
|
_migrate_v13_workspace_members_in_spaces(conn)
|
|
781
|
+
_migrate_v15_message_reactions(conn)
|
|
753
782
|
conn.commit()
|
|
754
783
|
finally:
|
|
755
784
|
conn.close()
|
|
@@ -197,6 +197,38 @@ class AdminRoutesMixin:
|
|
|
197
197
|
content_b64 = str(body.get("content_base64") or body.get("content") or "")
|
|
198
198
|
self._json(200, srv.file_upload_binary(rel, content_b64))
|
|
199
199
|
|
|
200
|
+
def _route_post_files_archive(self, body: dict) -> None:
|
|
201
|
+
srv = _srv()
|
|
202
|
+
if not srv._check_auth(self):
|
|
203
|
+
self._json(401, {"error": "unauthorized"})
|
|
204
|
+
return
|
|
205
|
+
paths = body.get("paths")
|
|
206
|
+
try:
|
|
207
|
+
archive = srv.files_archive(paths)
|
|
208
|
+
except PermissionError as exc:
|
|
209
|
+
self._json(403, {"ok": False, "error": "protected_file", "detail": str(exc)})
|
|
210
|
+
return
|
|
211
|
+
except ValueError as exc:
|
|
212
|
+
self._json(400, {"ok": False, "error": "invalid_selection", "detail": str(exc)})
|
|
213
|
+
return
|
|
214
|
+
self._send(200, archive, "application/zip")
|
|
215
|
+
|
|
216
|
+
def _route_post_files_delete(self, body: dict) -> None:
|
|
217
|
+
srv = _srv()
|
|
218
|
+
if not srv._check_auth(self):
|
|
219
|
+
self._json(401, {"error": "unauthorized"})
|
|
220
|
+
return
|
|
221
|
+
paths = body.get("paths")
|
|
222
|
+
try:
|
|
223
|
+
result = srv.files_delete(paths)
|
|
224
|
+
except PermissionError as exc:
|
|
225
|
+
self._json(403, {"ok": False, "error": "protected_file", "detail": str(exc)})
|
|
226
|
+
return
|
|
227
|
+
except ValueError as exc:
|
|
228
|
+
self._json(400, {"ok": False, "error": "invalid_selection", "detail": str(exc)})
|
|
229
|
+
return
|
|
230
|
+
self._json(200, result)
|
|
231
|
+
|
|
200
232
|
def _route_get_admin_vault_status(self, qs: dict[str, list[str]]) -> None:
|
|
201
233
|
srv = _srv()
|
|
202
234
|
if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
|
|
@@ -529,13 +529,16 @@ class AuthRoutesMixin:
|
|
|
529
529
|
)
|
|
530
530
|
display = str(body.get("display_name") or "").strip() if "display_name" in body else ""
|
|
531
531
|
tagline = str(body.get("tagline") or "").strip() if "tagline" in body else ""
|
|
532
|
-
srv.
|
|
533
|
-
runtime,
|
|
532
|
+
srv._apply_profile_identity(
|
|
534
533
|
native_slug,
|
|
535
534
|
display_name=display,
|
|
536
535
|
tagline=tagline,
|
|
537
|
-
user_soul=soul,
|
|
538
536
|
)
|
|
537
|
+
if soul:
|
|
538
|
+
result = srv.profile_soul_set(native_slug, soul)
|
|
539
|
+
if not result.get("ok"):
|
|
540
|
+
self._json(400, result)
|
|
541
|
+
return
|
|
539
542
|
profile_patch: dict[str, Any] = {}
|
|
540
543
|
if display:
|
|
541
544
|
profile_patch["display_name"] = display
|
|
@@ -552,6 +555,5 @@ class AuthRoutesMixin:
|
|
|
552
555
|
stamp = datetime.now(timezone.utc).isoformat()
|
|
553
556
|
row_patch = {**avatar_patch, "updated_at": stamp}
|
|
554
557
|
srv._upsert_agent_registry_row(native_slug, row_patch)
|
|
555
|
-
srv._upsert_agent_registry_row(runtime, row_patch)
|
|
556
558
|
srv._sync_agent_profile_db(native_slug, {"avatar_url": avatar_patch.get("avatar_url", "")})
|
|
557
559
|
self._json(200, {"ok": True, "runtime_profile": runtime})
|
|
@@ -223,17 +223,12 @@ class ChatRoutesMixin:
|
|
|
223
223
|
self._json(400, {"error": "model required"})
|
|
224
224
|
return
|
|
225
225
|
billing_provider = str(body.get("billing_provider", "")).strip()
|
|
226
|
-
#
|
|
227
|
-
# the
|
|
228
|
-
#
|
|
229
|
-
if
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
workspaces = srv._get_user_workspaces(user_id)
|
|
233
|
-
current = srv._resolve_current_workspace(user_id, workspaces)
|
|
234
|
-
workspace_id = str((current or {}).get("id") or (workspaces[0] or {}).get("id") or "")
|
|
235
|
-
profile = srv._runtime_profile_slug(user_id, profile)
|
|
236
|
-
srv.ensure_runtime_profile(profile, srv._runtime_template_slug(profile), user_id, workspace_id)
|
|
226
|
+
# Provider/model are shared agent settings. Credentials remain scoped
|
|
227
|
+
# to the acting user or company, so only workspace admins may mutate
|
|
228
|
+
# the agent template; the backend canonicalizes any stale u-* request.
|
|
229
|
+
if not selection_only and not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
|
|
230
|
+
self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
|
|
231
|
+
return
|
|
237
232
|
self._json(
|
|
238
233
|
200,
|
|
239
234
|
hermes_model_set(
|
|
@@ -267,14 +262,7 @@ class ChatRoutesMixin:
|
|
|
267
262
|
chain = body.get("chain", [])
|
|
268
263
|
selection_only = bool(body.get("selection_only"))
|
|
269
264
|
user_id = str(getattr(self, "auth_user", "") or "")
|
|
270
|
-
|
|
271
|
-
if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
|
|
272
|
-
if not workspace_id:
|
|
273
|
-
workspaces = srv._get_user_workspaces(user_id)
|
|
274
|
-
current = srv._resolve_current_workspace(user_id, workspaces)
|
|
275
|
-
workspace_id = str((current or {}).get("id") or (workspaces[0] or {}).get("id") or "")
|
|
276
|
-
profile = srv._runtime_profile_slug(user_id, profile)
|
|
277
|
-
srv.ensure_runtime_profile(profile, srv._runtime_template_slug(profile), user_id, workspace_id)
|
|
265
|
+
workspace_id = str(body.get("workspace_id", "")).strip()
|
|
278
266
|
if not isinstance(chain, list):
|
|
279
267
|
self._json(400, {"error": "chain must be a list"})
|
|
280
268
|
return
|
|
@@ -288,6 +276,7 @@ class ChatRoutesMixin:
|
|
|
288
276
|
chain,
|
|
289
277
|
selection_only=selection_only,
|
|
290
278
|
user_id=user_id,
|
|
279
|
+
workspace_id=workspace_id,
|
|
291
280
|
),
|
|
292
281
|
)
|
|
293
282
|
|
|
@@ -546,9 +535,7 @@ class ChatRoutesMixin:
|
|
|
546
535
|
return
|
|
547
536
|
template = resolve_validated_profile(profile_bootstrap_post.group(1))
|
|
548
537
|
user_id = str(getattr(self, "auth_user", "") or "").strip()
|
|
549
|
-
|
|
550
|
-
self._json(403, {"ok": False, "error": "native_agent_identity_owner_only"})
|
|
551
|
-
return
|
|
538
|
+
can_manage_identity = srv._role_allows(self, srv.OWNER_ADMIN_ROLES)
|
|
552
539
|
workspace_id = str(body.get("workspace_id") or "").strip()
|
|
553
540
|
if not workspace_id and user_id:
|
|
554
541
|
workspaces = srv._get_user_workspaces(user_id)
|
|
@@ -558,10 +545,14 @@ class ChatRoutesMixin:
|
|
|
558
545
|
self._json(400, {"error": "workspace_id and session required"})
|
|
559
546
|
return
|
|
560
547
|
model = str(body.get("model") or "").strip()
|
|
561
|
-
soul = str(body.get("soul") or "").strip()
|
|
562
|
-
display_name =
|
|
563
|
-
|
|
564
|
-
|
|
548
|
+
soul = str(body.get("soul") or "").strip() if can_manage_identity else ""
|
|
549
|
+
display_name = (
|
|
550
|
+
str(body.get("display_name") or body.get("room_name") or "").strip()
|
|
551
|
+
if can_manage_identity
|
|
552
|
+
else ""
|
|
553
|
+
)
|
|
554
|
+
role = str(body.get("role") or "").strip() if can_manage_identity else ""
|
|
555
|
+
tagline = str(body.get("tagline") or "").strip() if can_manage_identity else ""
|
|
565
556
|
lane = bootstrap_agent_dm_lane(
|
|
566
557
|
user_id,
|
|
567
558
|
workspace_id,
|