limixbot 0.3.24 → 0.3.25
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/dist/assets/{ActivityPanel-BjjmX49M.js → ActivityPanel-CgPkGKZZ.js} +1 -1
- package/dist/assets/{App-BWtQhGWp.js → App-lurtH-qI.js} +9 -9
- package/dist/assets/{AppEventEditor-Bq1-MGxi.js → AppEventEditor-G_71EWui.js} +1 -1
- package/dist/assets/BotSettingsDialog-BUvuLu53.js +16 -0
- package/dist/assets/{BrowserPanel-CPLcno_m.js → BrowserPanel-DrKZNyYB.js} +1 -1
- package/dist/assets/{BrowserProfilesManager-D9JsCp1k.js → BrowserProfilesManager-CtiUbbdv.js} +1 -1
- package/dist/assets/{CloudAddDialog-DcmN7A5a.js → CloudAddDialog-D4BX2927.js} +1 -1
- package/dist/assets/{ComputerFilesPane-Bjx6TGmM.js → ComputerFilesPane-RnmlJhlD.js} +1 -1
- package/dist/assets/{ComputerPanel-C9Xx4Ifs.js → ComputerPanel-D8upQSmd.js} +2 -2
- package/dist/assets/{DataPanel-IfpQCi3b.js → DataPanel-MZcku4SL.js} +1 -1
- package/dist/assets/{DesktopViewer-RjjUUlrQ.js → DesktopViewer-p7N7cqle.js} +1 -1
- package/dist/assets/{FilesView-D_0GwHAN.js → FilesView-CDbtoPxh.js} +1 -1
- package/dist/assets/{GrokMoveDialog-CUie-0en.js → GrokMoveDialog-BebZb43R.js} +1 -1
- package/dist/assets/{InspectorPanel-BuFL6wWf.js → InspectorPanel-BMkRIuTz.js} +1 -1
- package/dist/assets/{KeyboardShortcutsModal-uOhXagTA.js → KeyboardShortcutsModal-3XyVcNTZ.js} +1 -1
- package/dist/assets/{LiveDesktop-BwpZa-Ri.js → LiveDesktop-BkRR5lCn.js} +1 -1
- package/dist/assets/{LocalVmWorkspace-CousHKOb.js → LocalVmWorkspace-9l9iWtAy.js} +1 -1
- package/dist/assets/{MacLocalControl-BKDzplO2.js → MacLocalControl-B45d0l7H.js} +1 -1
- package/dist/assets/{MarkdownFilePreview-CCwc1Tek.js → MarkdownFilePreview-ChR-f9oD.js} +1 -1
- package/dist/assets/{MenuMotion-CSgaJLc2.js → MenuMotion-BBaxdyL_.js} +1 -1
- package/dist/assets/{NewBotDialog-BtvoCpqN.js → NewBotDialog-C_uQRhxy.js} +1 -1
- package/dist/assets/{PhoneAppDialog-C1hIwyT8.js → PhoneAppDialog-CDO1xUXD.js} +1 -1
- package/dist/assets/{PlacesRail-PP97GpM6.js → PlacesRail-iu-rjh5s.js} +1 -1
- package/dist/assets/{PluginsPanel-DpUEC1zc.js → PluginsPanel-DlQBRYQB.js} +1 -1
- package/dist/assets/{ProIntroduction-C-7uexIW.js → ProIntroduction-RbtTSiR6.js} +1 -1
- package/dist/assets/{RemoteAgentSettingsPanel-DtofJtgF.js → RemoteAgentSettingsPanel-C8r-80J4.js} +1 -1
- package/dist/assets/{RoomPanel-CHbgQ0xd.js → RoomPanel-BIUTnm5N.js} +1 -1
- package/dist/assets/{RoutinesPage-ZdFTYQc-.js → RoutinesPage-DROK-336.js} +1 -1
- package/dist/assets/{SettingsModal-Cdz3bEYZ.js → SettingsModal--yYGUYK2.js} +3 -3
- package/dist/assets/{ShareTeamDialog-CNu6Kwet.js → ShareTeamDialog-CVnQoRVQ.js} +1 -1
- package/dist/assets/{SignedInSites-CyWcDbLW.js → SignedInSites-B-iBIlfL.js} +1 -1
- package/dist/assets/TeamLibraryPanel-CwVaJVC_.js +1 -0
- package/dist/assets/{TeamMapPage-C2PnNdil.js → TeamMapPage-kXk9hCNu.js} +1 -1
- package/dist/assets/{TriggersPage-B_br6xBZ.js → TriggersPage-prZBUP3d.js} +1 -1
- package/dist/assets/{VisibilitySection-DH0WrBCn.js → VisibilitySection-DJjvTyLx.js} +1 -1
- package/dist/assets/{WebhookTrigger-BASBoMb_.js → WebhookTrigger-BAFikH71.js} +1 -1
- package/dist/assets/{WorksOnGrid-CboITAAs.js → WorksOnGrid-DJmfLtP8.js} +1 -1
- package/dist/assets/{abnfDiagram-YUKMZFIV-CJ69o_vp.js → abnfDiagram-YUKMZFIV-DUI6uevQ.js} +1 -1
- package/dist/assets/{activity-BdRjGS2C.js → activity-DVLFekjx.js} +1 -1
- package/dist/assets/{architectureDiagram-47P4ROYG-BH57AmKC.js → architectureDiagram-47P4ROYG-CMELW5_4.js} +1 -1
- package/dist/assets/{blockDiagram-E7TT5TSB-D3ebMs_o.js → blockDiagram-E7TT5TSB-C3xD0z1e.js} +1 -1
- package/dist/assets/{c4Diagram-CRA5TL53-6TrdcGZR.js → c4Diagram-CRA5TL53-Cm9X0GEV.js} +1 -1
- package/dist/assets/{calendar-days-DiMBTH2e.js → calendar-days-CDvwJ_d4.js} +1 -1
- package/dist/assets/channel-CsOUN1ZX.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BemwHdji.js → chunk-2Q5K7J3B-LWcbjcjL.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-CQZbLBld.js → chunk-5VM5RSS4-B3rNoWeE.js} +1 -1
- package/dist/assets/{chunk-7TKQ45FW-Dy6EXFTX.js → chunk-7TKQ45FW-FL9R0rHe.js} +1 -1
- package/dist/assets/{chunk-AW2ZBBNX-BbKGGyK8.js → chunk-AW2ZBBNX-JRef_4rd.js} +1 -1
- package/dist/assets/{chunk-FVRAUYC3-DHqUalc2.js → chunk-FVRAUYC3-C__loQFR.js} +2 -2
- package/dist/assets/{chunk-HTAEGDNF-B66Vjlwq.js → chunk-HTAEGDNF-Bx74_xmN.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DrtdT6L3.js → chunk-JWPE2WC7-B20vr3uH.js} +1 -1
- package/dist/assets/{chunk-KQW6MTUR-t7RW8fFT.js → chunk-KQW6MTUR-DfO9J2pt.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CntyS42N.js → chunk-XXDRQBXY-BAPGqnqj.js} +1 -1
- package/dist/assets/{classDiagram-v2-K4WV4PDN-KlNCgeTt.js → classDiagram-v2-K4WV4PDN-C3VR3yN8.js} +1 -1
- package/dist/assets/{coins-BHKpT34i.js → coins-CueFc0mY.js} +1 -1
- package/dist/assets/{core-DEy6KDLR.js → core-Bw6kaGwe.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-C_cXoOGT.js → cose-bilkent-JH36ORCC-19p3m04s.js} +1 -1
- package/dist/assets/{cynefin-EF2NZ3EQ-DGuwW5zt.js → cynefin-EF2NZ3EQ-Tv0tce7B.js} +1 -1
- package/dist/assets/{cynefinDiagram-3GCD6N5R-qMzxXkVk.js → cynefinDiagram-3GCD6N5R-Iin0JwlW.js} +1 -1
- package/dist/assets/{dagre-W4DXFKR2-Q-osY3Yi.js → dagre-W4DXFKR2-00TRmSmB.js} +1 -1
- package/dist/assets/{diagram-2UJZ2QOL-DyVJSBOF.js → diagram-2UJZ2QOL-DslMBjKt.js} +1 -1
- package/dist/assets/{diagram-OVF4WLC6-C5qBFdpm.js → diagram-OVF4WLC6-C3o3f1eL.js} +1 -1
- package/dist/assets/{diagram-PFPMY2P6-C1PW3U2P.js → diagram-PFPMY2P6-RRy9F9wH.js} +1 -1
- package/dist/assets/{diagram-UMYRVEAY-D2pa0Mzi.js → diagram-UMYRVEAY-CWfRKtkp.js} +1 -1
- package/dist/assets/{diagram-YEKJPTXX-BIzOA1li.js → diagram-YEKJPTXX-SlJKvYay.js} +1 -1
- package/dist/assets/{diagram-ZIFT7M5P-B1UL3O8m.js → diagram-ZIFT7M5P-DqzI8_eN.js} +1 -1
- package/dist/assets/{ebnfDiagram-VR2GEFS7-Cxu-kU8s.js → ebnfDiagram-VR2GEFS7-CDsrBNMu.js} +1 -1
- package/dist/assets/{elk-IJKZMXRS-DJS39Bm9.js → elk-IJKZMXRS-iikFqO-X.js} +1 -1
- package/dist/assets/{erDiagram-O2IAPWRE-Cb31BS5L.js → erDiagram-O2IAPWRE-DzFCc0Gp.js} +1 -1
- package/dist/assets/{file-up-DWGlVRGP.js → file-up-BzW-KJx-.js} +1 -1
- package/dist/assets/{flowDiagram-OXPTDLAJ-BaPTzS6m.js → flowDiagram-OXPTDLAJ-U5nLRus2.js} +1 -1
- package/dist/assets/{ganttDiagram-R7TSEDQI-B_Lr-26i.js → ganttDiagram-R7TSEDQI-h7xqrZ23.js} +1 -1
- package/dist/assets/{gitGraphDiagram-XJZIOB7I-UHyDFX6a.js → gitGraphDiagram-XJZIOB7I-Bo9ZX2g5.js} +1 -1
- package/dist/assets/{import-name-Cxwtm5XO.js → import-name-BN_-MYip.js} +1 -1
- package/dist/assets/{index-BSrb6nsc.js → index-BEjJ5c6H.js} +2 -2
- package/dist/assets/{index-BME9PdWM.js → index-DkDFNBNq.js} +1 -1
- package/dist/assets/{infoDiagram-5W2HQ5XZ-B7ovBJYA.js → infoDiagram-5W2HQ5XZ-CAZmFoiz.js} +1 -1
- package/dist/assets/{ishikawaDiagram-K3B6WC7H-BYS-TlrF.js → ishikawaDiagram-K3B6WC7H-T1M7DtvG.js} +1 -1
- package/dist/assets/{journeyDiagram-COXZFDF6-jJxKYEtT.js → journeyDiagram-COXZFDF6-DzO0Gzq9.js} +1 -1
- package/dist/assets/{kanban-definition-P3RFRI5V-DyiFYaIK.js → kanban-definition-P3RFRI5V-C0fM1UHu.js} +1 -1
- package/dist/assets/{mcp-servers-DXFuhTlS.js → mcp-servers-C7oRQpN5.js} +1 -1
- package/dist/assets/{mermaid.core-D3YJgsiB.js → mermaid.core-mKZTdBWz.js} +4 -4
- package/dist/assets/{mindmap-definition-LZFPQGKD-DnbWodUY.js → mindmap-definition-LZFPQGKD-DEgoC28p.js} +1 -1
- package/dist/assets/{minimize-Af1aU3Tz.js → minimize-D-CvaOV-.js} +1 -1
- package/dist/assets/{monitor-DuYHWIyw.js → monitor-DRcE92Rj.js} +1 -1
- package/dist/assets/{new-bot-TwOnvVO1.js → new-bot-COj1uEQa.js} +1 -1
- package/dist/assets/{org-library-BLD6YHcM.js → org-library-BIb28h24.js} +1 -1
- package/dist/assets/{package-format-72bbOy2w.js → package-format-hON5OXLZ.js} +1 -1
- package/dist/assets/{pegDiagram-BGZESJAR-CcOuP6zx.js → pegDiagram-BGZESJAR-Vatn56u_.js} +1 -1
- package/dist/assets/{pieDiagram-CAPJLFHJ-DhCU7Sfy.js → pieDiagram-CAPJLFHJ-DniOgFo3.js} +1 -1
- package/dist/assets/{quadrantDiagram-GDMTTRAM-7OziX26h.js → quadrantDiagram-GDMTTRAM-BqhAQt5w.js} +1 -1
- package/dist/assets/{rail-DI2iZJYt.js → rail-BWcrFQ2_.js} +1 -1
- package/dist/assets/{railroadDiagram-SM67HX2B-rg44R4U2.js → railroadDiagram-SM67HX2B-CxHs3JOs.js} +1 -1
- package/dist/assets/{remote-desktop-panel-BPKKvcHT.js → remote-desktop-panel-9XdtvJrd.js} +1 -1
- package/dist/assets/{requirementDiagram-X7JNWC4A-DFNvg72O.js → requirementDiagram-X7JNWC4A-ChjhMFon.js} +1 -1
- package/dist/assets/{sankeyDiagram-UM26HJRW-CUOrQF2i.js → sankeyDiagram-UM26HJRW-B67bYF8E.js} +1 -1
- package/dist/assets/{sequenceDiagram-ZO4K6R2Y-4HKsH1wt.js → sequenceDiagram-ZO4K6R2Y-CTy7B-eu.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BEXQDTx-.js → sizeCapture-INFHLROL-uy6qiY-2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-TBUQTH76-BYBLUqiy.js → stateDiagram-v2-TBUQTH76-BvYYPNqL.js} +1 -1
- package/dist/assets/{styles-C2_XZv1s.js → styles-DlOxUga6.js} +1 -1
- package/dist/assets/{swimlanes-N4OXWK64-D-bp8wWN.js → swimlanes-N4OXWK64-CMSAKu-2.js} +1 -1
- package/dist/assets/swimlanesDiagram-K3J5GTZL-BaaZdhur.js +8 -0
- package/dist/assets/{team-share-Dpu1nJNz.js → team-share-BbQqJ5O6.js} +1 -1
- package/dist/assets/{timeline-definition-YOQAKGHF-CtLFkcud.js → timeline-definition-YOQAKGHF-BOK1V5EC.js} +1 -1
- package/dist/assets/{usecaseDiagram-VIAY4XPW-AWOhUKvG.js → usecaseDiagram-VIAY4XPW-DEywfSfC.js} +1 -1
- package/dist/assets/{vennDiagram-BLWOH2XV-C_OYDBvp.js → vennDiagram-BLWOH2XV-DN1iWw97.js} +1 -1
- package/dist/assets/{vi-P4zxF4j0.js → vi-C-E3a4Tv.js} +2 -2
- package/dist/assets/{wardleyDiagram-YMQ3BMBF-CdxMh8tE.js → wardleyDiagram-YMQ3BMBF-BAIfK1We.js} +1 -1
- package/dist/assets/{xychartDiagram-TAQBALBS-BL2spC5z.js → xychartDiagram-TAQBALBS-BLBD8FaQ.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/rail.html +4 -4
- package/dist-server/index.js +244 -229
- package/dist-server/openmausbot.js +1 -1
- package/dist-server/package.json +1 -1
- package/dist-server/pair-cli.js +1 -1
- package/dist-server/server/imported-skills.js +7 -2
- package/dist-server/server/index.js +7 -5
- package/dist-server/server/limix-bot-library.js +6 -0
- package/dist-server/server/limix-skill-hub.js +2 -2
- package/dist-server/server/limix-skill-store.js +179 -0
- package/dist-server/server/routes/bot-skills.js +15 -4
- package/package.json +1 -1
- package/skills/windows-computer-automation/SKILL.md +53 -0
- package/skills/windows-computer-automation/assets/HERIONIX-LEADER-PROMPT.md +25 -0
- package/skills/windows-computer-automation/assets/control-policy.example.json +25 -0
- package/skills/windows-computer-automation/assets/mcp-servers.example.json +19 -0
- package/skills/windows-computer-automation/manifest.json +27 -0
- package/skills/windows-computer-automation/references/herionix-integration.md +72 -0
- package/skills/windows-computer-automation/references/routing.md +26 -0
- package/skills/windows-computer-automation/references/scenarios.md +44 -0
- package/skills/windows-computer-automation/references/security.md +24 -0
- package/skills/windows-computer-automation/scripts/policy_engine.py +235 -0
- package/skills/windows-computer-automation/scripts/test_policy_engine.py +107 -0
- package/dist/assets/BotSettingsDialog-CVlvG4Rb.js +0 -16
- package/dist/assets/TeamLibraryPanel-DMhdlaSH.js +0 -1
- package/dist/assets/channel-k2J2GMwv.js +0 -1
- package/dist/assets/swimlanesDiagram-K3J5GTZL-DTQBdgKt.js +0 -8
|
@@ -4193,7 +4193,7 @@ string(),skillManifestEntrySchema),stagedSkillWriteSchema=external_exports.objec
|
|
|
4193
4193
|
/^[a-f0-9]{64}$/).optional(),warnings:external_exports.array(external_exports.string()),skippedFiles:external_exports.array(external_exports.string()),createdAt:external_exports.string(),baseSha256:external_exports.string().regex(/^[a-f0-9]{64}$/).optional(),baseAppliedStageId:external_exports.string().optional()}).superRefine((entry,ctx)=>{entry.action==="update"&&(!entry.baseSha256||!entry.baseAppliedStageId)&&ctx.addIssue({code:"custom",message:"updated skills require their reviewed base revi\
|
|
4194
4194
|
sion"})}),stagedStoreSchema=external_exports.object({writes:external_exports.record(external_exports.string(),stagedSkillWriteSchema)})}});import{dirname as dirname29,isAbsolute as isAbsolute14,join as join64,resolve as resolve22}from"node:path";var IMPORTED_SKILLS_DIR,INDEX_PATH,sha256Hex,entrySchema,indexSchema,choicesSchema2,init_imported_skills=__esm({"server/imported-skills.ts"(){"use strict";init_zod();init_skill_fit();init_skill_md();init_atomic();init_config3();init_skill_storage();init_skills();IMPORTED_SKILLS_DIR=join64(DATA_DIR,"imported-skills"),INDEX_PATH=join64(DATA_DIR,"skill-state","imported-skills.json"),sha256Hex=
|
|
4195
4195
|
external_exports.string().regex(/^[a-f0-9]{64}$/),entrySchema=external_exports.object({description:external_exports.string(),source:external_exports.enum(["claudeCode","codex"]),root:external_exports.string(),origin:external_exports.string(),label:external_exports.string(),sha256:sha256Hex,filesSha256:sha256Hex,importedAt:external_exports.string(),warnings:external_exports.array(external_exports.string()),license:external_exports.string().optional(),compatibility:external_exports.string().optional()}),
|
|
4196
|
-
indexSchema=external_exports.record(external_exports.string(),entrySchema),choicesSchema2=external_exports.object({off:external_exports.array(external_exports.string()),on:external_exports.record(external_exports.string(),sha256Hex).default({}),listed:external_exports.array(external_exports.string()).optional()})}});function parseToml(source){let text13=source.charCodeAt(0)===65279?source.slice(1):source,i2=0,root=table(),current=root,fail3=message5=>{throw new TomlError(`line ${text13.slice(0,i2).split(`
|
|
4196
|
+
indexSchema=external_exports.record(external_exports.string(),entrySchema),choicesSchema2=external_exports.object({off:external_exports.array(external_exports.string()),on:external_exports.record(external_exports.string(),sha256Hex).default({}),listed:external_exports.array(external_exports.string()).optional(),picked:external_exports.array(external_exports.string()).optional()})}});function parseToml(source){let text13=source.charCodeAt(0)===65279?source.slice(1):source,i2=0,root=table(),current=root,fail3=message5=>{throw new TomlError(`line ${text13.slice(0,i2).split(`
|
|
4197
4197
|
`).length}: ${message5}`)},spaces=()=>{for(;text13[i2]===" "||text13[i2]===" ";)i2++},comment=()=>{if(text13[i2]==="#")for(;i2<text13.length&&text13[i2]!==`
|
|
4198
4198
|
`;)i2++},newline=()=>{if(text13[i2]===`
|
|
4199
4199
|
`)i2+=1;else if(text13[i2]==="\r"&&text13[i2+1]===`
|
package/dist-server/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"type":"module","version":"0.3.
|
|
1
|
+
{"type":"module","version":"0.3.25"}
|
package/dist-server/pair-cli.js
CHANGED
|
@@ -4192,7 +4192,7 @@ string(),skillManifestEntrySchema),stagedSkillWriteSchema=external_exports.objec
|
|
|
4192
4192
|
/^[a-f0-9]{64}$/).optional(),warnings:external_exports.array(external_exports.string()),skippedFiles:external_exports.array(external_exports.string()),createdAt:external_exports.string(),baseSha256:external_exports.string().regex(/^[a-f0-9]{64}$/).optional(),baseAppliedStageId:external_exports.string().optional()}).superRefine((entry,ctx)=>{entry.action==="update"&&(!entry.baseSha256||!entry.baseAppliedStageId)&&ctx.addIssue({code:"custom",message:"updated skills require their reviewed base revi\
|
|
4193
4193
|
sion"})}),stagedStoreSchema=external_exports.object({writes:external_exports.record(external_exports.string(),stagedSkillWriteSchema)})}});import{dirname as dirname29,isAbsolute as isAbsolute14,join as join64,resolve as resolve22}from"node:path";var IMPORTED_SKILLS_DIR,INDEX_PATH,sha256Hex,entrySchema,indexSchema,choicesSchema2,init_imported_skills=__esm({"server/imported-skills.ts"(){"use strict";init_zod();init_skill_fit();init_skill_md();init_atomic();init_config3();init_skill_storage();init_skills();IMPORTED_SKILLS_DIR=join64(DATA_DIR,"imported-skills"),INDEX_PATH=join64(DATA_DIR,"skill-state","imported-skills.json"),sha256Hex=
|
|
4194
4194
|
external_exports.string().regex(/^[a-f0-9]{64}$/),entrySchema=external_exports.object({description:external_exports.string(),source:external_exports.enum(["claudeCode","codex"]),root:external_exports.string(),origin:external_exports.string(),label:external_exports.string(),sha256:sha256Hex,filesSha256:sha256Hex,importedAt:external_exports.string(),warnings:external_exports.array(external_exports.string()),license:external_exports.string().optional(),compatibility:external_exports.string().optional()}),
|
|
4195
|
-
indexSchema=external_exports.record(external_exports.string(),entrySchema),choicesSchema2=external_exports.object({off:external_exports.array(external_exports.string()),on:external_exports.record(external_exports.string(),sha256Hex).default({}),listed:external_exports.array(external_exports.string()).optional()})}});function parseToml(source){let text13=source.charCodeAt(0)===65279?source.slice(1):source,i2=0,root=table(),current=root,fail3=message5=>{throw new TomlError(`line ${text13.slice(0,i2).split(`
|
|
4195
|
+
indexSchema=external_exports.record(external_exports.string(),entrySchema),choicesSchema2=external_exports.object({off:external_exports.array(external_exports.string()),on:external_exports.record(external_exports.string(),sha256Hex).default({}),listed:external_exports.array(external_exports.string()).optional(),picked:external_exports.array(external_exports.string()).optional()})}});function parseToml(source){let text13=source.charCodeAt(0)===65279?source.slice(1):source,i2=0,root=table(),current=root,fail3=message5=>{throw new TomlError(`line ${text13.slice(0,i2).split(`
|
|
4196
4196
|
`).length}: ${message5}`)},spaces=()=>{for(;text13[i2]===" "||text13[i2]===" ";)i2++},comment=()=>{if(text13[i2]==="#")for(;i2<text13.length&&text13[i2]!==`
|
|
4197
4197
|
`;)i2++},newline=()=>{if(text13[i2]===`
|
|
4198
4198
|
`)i2+=1;else if(text13[i2]==="\r"&&text13[i2+1]===`
|
|
@@ -47,6 +47,8 @@ const choicesSchema = z.object({
|
|
|
47
47
|
off: z.array(z.string()),
|
|
48
48
|
on: z.record(z.string(), sha256Hex).default({}),
|
|
49
49
|
listed: z.array(z.string()).optional(),
|
|
50
|
+
/** Limix Bot: skills from this computer are off for a bot until a person adds one from its store. */
|
|
51
|
+
picked: z.array(z.string()).optional(),
|
|
50
52
|
});
|
|
51
53
|
/** Every imported skill by name; empty when there are none or the index cannot be trusted. */
|
|
52
54
|
export function readImportedSkills() {
|
|
@@ -279,6 +281,8 @@ export function storeImportedSkills(inputs) {
|
|
|
279
281
|
function enabledFor(name, entry, choices) {
|
|
280
282
|
if (choices.off.includes(name))
|
|
281
283
|
return false;
|
|
284
|
+
if (!(choices.picked ?? []).includes(name))
|
|
285
|
+
return false;
|
|
282
286
|
return entry.warnings.length === 0 || choices.on[name] === entry.filesSha256;
|
|
283
287
|
}
|
|
284
288
|
const comparable = (path) => {
|
|
@@ -361,9 +365,10 @@ export function setImportedSkillEnabled(botId, name, enabled) {
|
|
|
361
365
|
const off = choices.off.filter((other) => other !== name);
|
|
362
366
|
const { [name]: _read, ...on } = choices.on;
|
|
363
367
|
const listed = choices.listed?.filter((other) => other !== name);
|
|
368
|
+
const picked = (choices.picked ?? []).filter((other) => other !== name);
|
|
364
369
|
const next = enabled
|
|
365
|
-
? { off, on: entry.warnings.length ? { ...on, [name]: entry.filesSha256 } : on, ...(listed ? { listed: [...listed, name].sort() } : {}) }
|
|
366
|
-
: { off: [...off, name].sort(), on, ...(listed ? { listed } : {}) };
|
|
370
|
+
? { off, on: entry.warnings.length ? { ...on, [name]: entry.filesSha256 } : on, ...(listed ? { listed: [...listed, name].sort() } : {}), picked: [...picked, name].sort() }
|
|
371
|
+
: { off: [...off, name].sort(), on, ...(listed ? { listed } : {}), picked };
|
|
367
372
|
mkdirSync(dirname(choicesPath(botId)), { recursive: true, mode: 0o700 });
|
|
368
373
|
writeFileAtomic(choicesPath(botId), `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
369
374
|
return importedSkillListings(botId, new Set()).find((skill) => skill.name === name);
|
|
@@ -442,7 +442,7 @@ import { AGENTS_NODE_FLAG, SPAWNED_PROXIES } from "./proxy-paths.js";
|
|
|
442
442
|
import { agentsProxyLaunch, agentsToolBridgeEnv, applyAgentsBridge, bridgedToolTitle, turnEngine } from "./agents-bridge-switch.js";
|
|
443
443
|
import { agentsServerEnv, botDrivenConversation, COMPUTER_OFF_REFUSAL, computerPickRefusal, directTurnKind, roomSkillsRefusal } from "./agents-turn.js";
|
|
444
444
|
import { harnessMcpTokenEnv } from "./harness-mcp-token.js";
|
|
445
|
-
import { loadBundledSkills, loadUserSkills, mergeSkills, renderSkillInstructions,
|
|
445
|
+
import { loadBundledSkills, loadUserSkills, mergeSkills, renderSkillInstructions, skillsLibraryEnabled, readSkillLibraryIndex, } from "./skill-library.js";
|
|
446
446
|
import { runSkillsLibraryBootSweep } from "./skills-library-migration.js";
|
|
447
447
|
import { installedPlaybookInstructions } from "./installed-playbooks.js";
|
|
448
448
|
import { picture as sharedPicture } from "./package-export.js";
|
|
@@ -512,6 +512,7 @@ import { createBotOrderRoutes } from "./routes/bot-order.js";
|
|
|
512
512
|
import { createBotLifecycleRoutes } from "./routes/bot-lifecycle.js";
|
|
513
513
|
import { createBotSkillsRoutes } from "./routes/bot-skills.js";
|
|
514
514
|
import { startHubAutoSync } from "./limix-skill-hub.js";
|
|
515
|
+
import { limixBundledFor, limixSelectBundled, limixSkillDefaultsMigration } from "./limix-skill-store.js";
|
|
515
516
|
import { createSkillShareRoutes } from "./routes/skill-share.js";
|
|
516
517
|
import { createTeamContextRoutes } from "./routes/team-context.js";
|
|
517
518
|
import { createAppSuggestions } from "./host/app-suggestions.js";
|
|
@@ -1436,6 +1437,7 @@ function resyncOpenCodeProviderKeys() {
|
|
|
1436
1437
|
const firstEngineRead = holdUntilFirstEngineRead();
|
|
1437
1438
|
await registry.load(providerConfigs(), decorateHostedProvider);
|
|
1438
1439
|
const bundledSkills = loadBundledSkills();
|
|
1440
|
+
limixSkillDefaultsMigration();
|
|
1439
1441
|
const availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join(DATA_DIR, "skills")));
|
|
1440
1442
|
/** Every turn the engine can mount it, whatever its words: they pick only the skill's instructions. A Cloud home or a hosted workspace has no USB port. */
|
|
1441
1443
|
const usbPhoneOffered = (instance, skills) => instance.adapter.capabilities.phoneMcp === true &&
|
|
@@ -8084,8 +8086,8 @@ async function beginTurn(botId, text, opts) {
|
|
|
8084
8086
|
handoffs.begin(threadId, dispatchClaimId, dispatchContext.handoff);
|
|
8085
8087
|
const integrations = {};
|
|
8086
8088
|
let browser = null;
|
|
8087
|
-
const skills = availableSkills(), phoneOffered = usbPhoneOffered(instance, skills) && !proactive.fence.confines(threadId) && !botApiThread(threadId);
|
|
8088
|
-
const selectedSkills =
|
|
8089
|
+
const skills = limixBundledFor(bot.id, availableSkills()), phoneOffered = usbPhoneOffered(instance, skills) && !proactive.fence.confines(threadId) && !botApiThread(threadId);
|
|
8090
|
+
const selectedSkills = limixSelectBundled(providerText, [
|
|
8089
8091
|
...(phoneOffered ? ["phoneMcp"] : []),
|
|
8090
8092
|
...(skillAuthoring ? ["skillAuthoring"] : []),
|
|
8091
8093
|
], skills);
|
|
@@ -10048,8 +10050,8 @@ setupRetry = 0) {
|
|
|
10048
10050
|
: undefined, bot.id);
|
|
10049
10051
|
const roomContext = roomWindow.text;
|
|
10050
10052
|
const turnImages = usesNativeImageInput ? [...burstImages, ...resolvedLatestImages.images, ...await videoFramesWithin(burstImages.length + resolvedLatestImages.images.length, resolvedLatestImages.videos)] : [];
|
|
10051
|
-
const skills = availableSkills(), phoneOffered = usbPhoneOffered(instance, skills);
|
|
10052
|
-
const selectedSkills = mergeSkills(
|
|
10053
|
+
const skills = limixBundledFor(bot.id, availableSkills()), phoneOffered = usbPhoneOffered(instance, skills);
|
|
10054
|
+
const selectedSkills = mergeSkills(limixSelectBundled(roomContext, phoneOffered ? ["phoneMcp"] : [], skills), limixSelectBundled(latestUser?.text ?? "", skillAuthoring ? ["skillAuthoring"] : [], skills));
|
|
10053
10055
|
if (phoneOffered)
|
|
10054
10056
|
integrations.phone = phoneIntegration(bot.id, threadId, internalGeneration);
|
|
10055
10057
|
try {
|
|
@@ -123,6 +123,12 @@ export function parseLibraryBot(value, expectedId) {
|
|
|
123
123
|
schedule: { type: "daily", time, weekdays: [...new Set(weekdays)].sort() }, enabled: false, overlap: "skip" };
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
|
+
if (value.skills !== undefined) {
|
|
127
|
+
if (!Array.isArray(value.skills) || value.skills.length > 20 || value.skills.some((name) => typeof name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || name.length > 64)) {
|
|
128
|
+
throw new Error("skills is invalid");
|
|
129
|
+
}
|
|
130
|
+
bot.skills = [...new Set(value.skills)];
|
|
131
|
+
}
|
|
126
132
|
return bot;
|
|
127
133
|
}
|
|
128
134
|
async function fetchJson(url, maxBytes) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Limix Skill Hub client: keeps a cached copy of the hub catalog, installs a skill only after every file matches the
|
|
2
2
|
// SHA-256 the catalog pins, and never installs scripts, binaries or MCP files unless the owner allows tools for that
|
|
3
|
-
// one skill. Installed skills
|
|
3
|
+
// one skill. Installed skills wait in the store ("Kho Kĩ Năng (Skills)"), switched on but on no bot until one is added to it. Auto Sync (catalog refresh) is on by default,
|
|
4
4
|
// Auto Install is off by default.
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
@@ -171,7 +171,7 @@ export async function installHubSkill(id, options = {}) {
|
|
|
171
171
|
...(entry.tags.length ? { tags: entry.tags } : {}),
|
|
172
172
|
warnings: entry.notes,
|
|
173
173
|
skippedFiles: skipped,
|
|
174
|
-
reviewState: "
|
|
174
|
+
reviewState: "approved",
|
|
175
175
|
...(files.length ? { files } : {}),
|
|
176
176
|
});
|
|
177
177
|
if ("error" in result)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Limix Bot: skills are chosen per bot. A new bot starts with only Limix's core skills (learning new skills, reading
|
|
2
|
+
// links with Agent Reach, operating Windows, the phone helpers) plus the skills it learns itself; everything else waits
|
|
3
|
+
// in the store ("Kho Kĩ Năng (Skills)") until a person adds it to one bot. Settings → Kĩ Năng (Skills) only downloads and
|
|
4
|
+
// installs skills into the store; it never turns a skill on for every bot.
|
|
5
|
+
//
|
|
6
|
+
// Three kinds of skill can be added to a bot from the store:
|
|
7
|
+
// bundled — shipped in skills/<id> (ui-ux-pro-max, data-dashboards…): the bot's picks live in
|
|
8
|
+
// <data>/skill-state/<bot>/limix-bundled.json; core ones are always on and cannot be removed.
|
|
9
|
+
// library — installed from the Limix Skill Hub or pasted in Settings: the bot's assignedSkills.
|
|
10
|
+
// imported — brought in from this computer's Claude Code or Codex: the bot's own switch (opt-in in Limix).
|
|
11
|
+
// `/name` calls any skill the bot has, bundled ones included (limixSelectBundled).
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { isSkillName } from "../shared/skill-md.js";
|
|
15
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
16
|
+
import { DATA_DIR } from "./config.js";
|
|
17
|
+
import { importedSkillListings, setImportedSkillEnabled } from "./imported-skills.js";
|
|
18
|
+
import { listLibrarySkills, loadBundledSkills, mergeSkills, readSkillLibraryIndex, selectBundledSkills, setLibrarySkillEveryBot, setLibrarySkillOptOut, setLibrarySkillReviewState, unassignLibrarySkill, } from "./skill-library.js";
|
|
19
|
+
/** Always on for every bot. "Learning new skills" itself is the skill-authoring feature, on for every bot already. */
|
|
20
|
+
export const LIMIX_CORE_SKILLS = new Set([
|
|
21
|
+
"create-verification-skill", "agent-reach", "windows-computer-automation", "phone-connection", "phone-harness",
|
|
22
|
+
]);
|
|
23
|
+
const BOT_ID = /^[\w-]+$/;
|
|
24
|
+
const picksPath = (botId) => join(DATA_DIR, "skill-state", botId, "limix-bundled.json");
|
|
25
|
+
export function bundledPicks(botId) {
|
|
26
|
+
if (!BOT_ID.test(botId))
|
|
27
|
+
return new Set();
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(readFileSync(picksPath(botId), "utf8"));
|
|
30
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((name) => typeof name === "string" && isSkillName(name)) : []);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return new Set();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function setBundledPick(botId, id, on) {
|
|
37
|
+
if (!BOT_ID.test(botId) || !isSkillName(id))
|
|
38
|
+
return;
|
|
39
|
+
const picks = bundledPicks(botId);
|
|
40
|
+
if (picks.has(id) === on)
|
|
41
|
+
return;
|
|
42
|
+
if (on)
|
|
43
|
+
picks.add(id);
|
|
44
|
+
else
|
|
45
|
+
picks.delete(id);
|
|
46
|
+
mkdirSync(join(DATA_DIR, "skill-state", botId), { recursive: true, mode: 0o700 });
|
|
47
|
+
writeFileAtomic(picksPath(botId), `${JSON.stringify([...picks].sort(), null, 2)}\n`, { mode: 0o600 });
|
|
48
|
+
}
|
|
49
|
+
/** The shipped skills one bot has: the core ones and the ones a person added to it. */
|
|
50
|
+
export function limixBundledFor(botId, skills) {
|
|
51
|
+
const picks = bundledPicks(botId);
|
|
52
|
+
return skills.filter(({ manifest }) => LIMIX_CORE_SKILLS.has(manifest.id) || picks.has(manifest.id));
|
|
53
|
+
}
|
|
54
|
+
const slashCalled = (text, id) => new RegExp(`(^|\\s)/${id.replace(/[-]/g, "\\-")}(?=$|[\\s.,:;!?])`, "m").test(text);
|
|
55
|
+
/** selectBundledSkills, plus every skill of `skills` the text calls by `/id`. */
|
|
56
|
+
export function limixSelectBundled(text, capabilities, skills) {
|
|
57
|
+
const available = new Set(capabilities);
|
|
58
|
+
const called = skills.filter(({ manifest }) => manifest.requiredCapabilities.every((capability) => available.has(capability)) && slashCalled(text, manifest.id));
|
|
59
|
+
return mergeSkills(selectBundledSkills(text, available, skills), called);
|
|
60
|
+
}
|
|
61
|
+
/** The bot's shipped skills as rows of its Skills page. */
|
|
62
|
+
export function limixBundledListings(botId) {
|
|
63
|
+
return limixBundledFor(botId, loadBundledSkills()).map(({ manifest }) => ({
|
|
64
|
+
name: manifest.id, description: manifest.description, enabled: true, editable: false,
|
|
65
|
+
source: LIMIX_CORE_SKILLS.has(manifest.id) ? "Kĩ năng gốc của Limix Bot" : "Kho Kĩ Năng (Skills) · có sẵn",
|
|
66
|
+
sha256: "", importedAt: "", warnings: [], skippedFiles: [], origin: "bundled", core: LIMIX_CORE_SKILLS.has(manifest.id),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
/** A bot's switch for one of its shipped skills: off takes an added one away; a core one stays. Null: not shipped. */
|
|
70
|
+
export function limixBundledSwitch(botId, name, enabled) {
|
|
71
|
+
if (!loadBundledSkills().some(({ manifest }) => manifest.id === name))
|
|
72
|
+
return null;
|
|
73
|
+
if (LIMIX_CORE_SKILLS.has(name)) {
|
|
74
|
+
return enabled ? { status: 200, body: { skill: limixBundledListings(botId).find((skill) => skill.name === name) } }
|
|
75
|
+
: { status: 409, body: { error: "Kĩ năng gốc của Limix Bot luôn bật cho mọi bot." } };
|
|
76
|
+
}
|
|
77
|
+
setBundledPick(botId, name, enabled);
|
|
78
|
+
return { status: 200, body: { skill: { ...limixBundledListings(botId).find((skill) => skill.name === name), name, enabled } } };
|
|
79
|
+
}
|
|
80
|
+
function storeItems(bots, botId) {
|
|
81
|
+
const bot = bots.bot(botId);
|
|
82
|
+
const assigned = new Set(bot.assignedSkills ?? []);
|
|
83
|
+
const picks = bundledPicks(botId);
|
|
84
|
+
const items = new Map();
|
|
85
|
+
for (const { manifest } of loadBundledSkills()) {
|
|
86
|
+
const core = LIMIX_CORE_SKILLS.has(manifest.id);
|
|
87
|
+
items.set(manifest.id, { name: manifest.id, description: manifest.description, kind: "bundled", added: core || picks.has(manifest.id), core, ready: true, warnings: 0, source: "Có sẵn trong Limix Bot" });
|
|
88
|
+
}
|
|
89
|
+
for (const skill of listLibrarySkills()) {
|
|
90
|
+
if (items.has(skill.name))
|
|
91
|
+
continue;
|
|
92
|
+
const intact = !skill.warnings.some((warning) => warning.includes("enablement is blocked"));
|
|
93
|
+
items.set(skill.name, {
|
|
94
|
+
name: skill.name, description: skill.description, kind: "library", added: assigned.has(skill.name) && skill.enabled, core: false,
|
|
95
|
+
ready: intact, warnings: skill.warnings.length, source: skill.source.startsWith("limix-hub:") ? "Thư viện kĩ năng Limix" : "Đã cài trong Cài đặt",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
for (const skill of importedSkillListings(botId, new Set())) {
|
|
99
|
+
if (items.has(skill.name))
|
|
100
|
+
continue;
|
|
101
|
+
items.set(skill.name, { name: skill.name, description: skill.description, kind: "imported", added: skill.enabled, core: false, ready: true, warnings: skill.warnings.length, source: skill.source || "Từ máy tính này" });
|
|
102
|
+
}
|
|
103
|
+
return [...items.values()].sort((a, b) => Number(b.core) - Number(a.core) || a.name.localeCompare(b.name));
|
|
104
|
+
}
|
|
105
|
+
/** Add one skill to one bot, or take it away. `kind` may be left out: the store finds the name. */
|
|
106
|
+
export function limixStoreSet(bots, botId, name, add, kind) {
|
|
107
|
+
const bot = bots.bot(botId);
|
|
108
|
+
if (!bot)
|
|
109
|
+
return { error: "Không tìm thấy bot." };
|
|
110
|
+
if (!isSkillName(name))
|
|
111
|
+
return { error: "Tên kĩ năng không hợp lệ." };
|
|
112
|
+
const item = storeItems(bots, botId).find((entry) => entry.name === name && (!kind || entry.kind === kind));
|
|
113
|
+
if (!item)
|
|
114
|
+
return { error: `Kho chưa có kĩ năng "${name}".` };
|
|
115
|
+
if (item.kind === "bundled") {
|
|
116
|
+
if (item.core)
|
|
117
|
+
return add ? { ok: true } : { error: "Kĩ năng gốc của Limix Bot luôn bật cho mọi bot." };
|
|
118
|
+
setBundledPick(botId, name, add);
|
|
119
|
+
return { ok: true };
|
|
120
|
+
}
|
|
121
|
+
if (item.kind === "imported") {
|
|
122
|
+
const result = setImportedSkillEnabled(botId, name, add);
|
|
123
|
+
return "error" in result ? { error: result.error } : { ok: true };
|
|
124
|
+
}
|
|
125
|
+
const assigned = bot.assignedSkills ?? [];
|
|
126
|
+
if (add) {
|
|
127
|
+
// Adding from the store is the person's choice for this bot, so a skill installed but not yet switched on is switched on.
|
|
128
|
+
const listing = listLibrarySkills().find((skill) => skill.name === name);
|
|
129
|
+
if (listing && !listing.enabled) {
|
|
130
|
+
const approved = setLibrarySkillReviewState(name, "approved");
|
|
131
|
+
if ("error" in approved)
|
|
132
|
+
return { error: approved.error };
|
|
133
|
+
}
|
|
134
|
+
setLibrarySkillOptOut(botId, name, false);
|
|
135
|
+
if (!assigned.includes(name))
|
|
136
|
+
bots.patchBot(botId, { assignedSkills: [...assigned, name] });
|
|
137
|
+
return { ok: true };
|
|
138
|
+
}
|
|
139
|
+
const result = unassignLibrarySkill(botId, name, assigned);
|
|
140
|
+
if ("unassigned" in result && result.assignedSkills)
|
|
141
|
+
bots.patchBot(botId, { assignedSkills: result.assignedSkills });
|
|
142
|
+
return { ok: true };
|
|
143
|
+
}
|
|
144
|
+
/** GET /api/limix/skill-store/<bot> lists the store for that bot; POST {name, add, kind?} adds or takes away one skill. */
|
|
145
|
+
export function handleLimixSkillStore(bots, method, path, body) {
|
|
146
|
+
const m = path.match(/^\/api\/limix\/skill-store\/([\w-]+)$/);
|
|
147
|
+
if (!m)
|
|
148
|
+
return null;
|
|
149
|
+
const botId = m[1];
|
|
150
|
+
if (!bots.bot(botId))
|
|
151
|
+
return { status: 404, body: { error: "Không tìm thấy bot." } };
|
|
152
|
+
if (method === "GET")
|
|
153
|
+
return { status: 200, body: { skills: storeItems(bots, botId) } };
|
|
154
|
+
if (method === "POST") {
|
|
155
|
+
const raw = (body && typeof body === "object" ? body : {});
|
|
156
|
+
const kind = raw.kind === "bundled" || raw.kind === "library" || raw.kind === "imported" ? raw.kind : undefined;
|
|
157
|
+
if (typeof raw.name !== "string" || typeof raw.add !== "boolean")
|
|
158
|
+
return { status: 400, body: { error: "Cần tên kĩ năng và add." } };
|
|
159
|
+
const result = limixStoreSet(bots, botId, raw.name, raw.add, kind);
|
|
160
|
+
return "error" in result ? { status: 422, body: result } : { status: 200, body: { skills: storeItems(bots, botId) } };
|
|
161
|
+
}
|
|
162
|
+
return { status: 405, body: { error: "method not allowed" } };
|
|
163
|
+
}
|
|
164
|
+
/** Once: skills that earlier versions turned on for every bot go back to the store. */
|
|
165
|
+
export function limixSkillDefaultsMigration() {
|
|
166
|
+
const marker = join(DATA_DIR, "skill-state", "limix-skill-defaults-v1.json");
|
|
167
|
+
if (existsSync(marker))
|
|
168
|
+
return;
|
|
169
|
+
try {
|
|
170
|
+
for (const [name, entry] of Object.entries(readSkillLibraryIndex()))
|
|
171
|
+
if (entry.everyBot)
|
|
172
|
+
setLibrarySkillEveryBot(name, false);
|
|
173
|
+
mkdirSync(join(DATA_DIR, "skill-state"), { recursive: true, mode: 0o700 });
|
|
174
|
+
writeFileAtomic(marker, `${JSON.stringify({ at: new Date().toISOString() })}\n`, { mode: 0o600 });
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Retried at the next start.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -7,6 +7,7 @@ import { fetchSkillFromSource } from "../skill-fetch.js";
|
|
|
7
7
|
import { readLocalSkills } from "../limix-skill-folder.js";
|
|
8
8
|
import { loadBundledSkills } from "../skill-library.js";
|
|
9
9
|
import { handleLimixHub } from "../limix-skill-hub.js";
|
|
10
|
+
import { handleLimixSkillStore, LIMIX_CORE_SKILLS, limixBundledListings, limixBundledSwitch } from "../limix-skill-store.js";
|
|
10
11
|
import { skillImportSummary } from "../skill-folder.js";
|
|
11
12
|
import { installLibrarySkill, librarySkillOptOuts, listLibrarySkills, readLibrarySkillFile, readSkillLibraryIndex, setLibrarySkillEveryBot, setLibrarySkillOptOut, setLibrarySkillReviewState, skillsLibraryEnabled, unassignLibrarySkill } from "../skill-library.js";
|
|
12
13
|
import { installSkill, listBotSkillsWithOrigin, listStagedSkillWrites, parseSkillMd, removeSkill, scanSkillText, setSkillEnabled, } from "../skills.js";
|
|
@@ -25,8 +26,13 @@ export function createBotSkillsRoutes(deps) {
|
|
|
25
26
|
if (hub)
|
|
26
27
|
return json(res, hub.status, hub.body);
|
|
27
28
|
}
|
|
29
|
+
if (path.startsWith("/api/limix/skill-store/")) {
|
|
30
|
+
const skillStore = handleLimixSkillStore(store, method, path, method === "POST" ? await readBody(req) : undefined);
|
|
31
|
+
if (skillStore)
|
|
32
|
+
return json(res, skillStore.status, skillStore.body);
|
|
33
|
+
}
|
|
28
34
|
if (method === "GET" && path === "/api/limix/bundled-skills") {
|
|
29
|
-
const skills = loadBundledSkills().map(({ manifest }) => ({ id: manifest.id, name: manifest.name, description: manifest.description, defaultEnabled: manifest.defaultEnabled, triggerTerms: manifest.triggerTerms }));
|
|
35
|
+
const skills = loadBundledSkills().map(({ manifest }) => ({ id: manifest.id, name: manifest.name, description: manifest.description, defaultEnabled: manifest.defaultEnabled, triggerTerms: manifest.triggerTerms, core: LIMIX_CORE_SKILLS.has(manifest.id) }));
|
|
30
36
|
return json(res, 200, { skills });
|
|
31
37
|
}
|
|
32
38
|
// Imports land DISABLED: a person enables one after reading SKILL.md and its scan warnings.
|
|
@@ -63,14 +69,15 @@ export function createBotSkillsRoutes(deps) {
|
|
|
63
69
|
const { skills, library } = listBotSkillsWithOrigin(m[1], store.bot(m[1]).assignedSkills);
|
|
64
70
|
const names = new Set(skills.map((skill) => skill.name));
|
|
65
71
|
return json(res, 200, {
|
|
66
|
-
skills: [...skills, ...listBotSkills(bot, threadId).filter((skill) => !names.has(skill.name))
|
|
72
|
+
skills: [...skills, ...listBotSkills(bot, threadId).filter((skill) => !names.has(skill.name)),
|
|
73
|
+
...limixBundledListings(bot.id).filter((skill) => !names.has(skill.name) && !listBotSkills(bot, threadId).some((other) => other.name === skill.name))],
|
|
67
74
|
library,
|
|
68
75
|
assignedSkills: store.bot(m[1]).assignedSkills,
|
|
69
76
|
staged: listStagedSkillWrites(m[1]).map(stagedSkillListing),
|
|
70
77
|
});
|
|
71
78
|
}
|
|
72
79
|
return json(res, 200, {
|
|
73
|
-
skills: listBotSkills(bot, threadId),
|
|
80
|
+
skills: [...listBotSkills(bot, threadId), ...limixBundledListings(bot.id)],
|
|
74
81
|
staged: listStagedSkillWrites(m[1]).map(stagedSkillListing),
|
|
75
82
|
});
|
|
76
83
|
}
|
|
@@ -121,8 +128,12 @@ export function createBotSkillsRoutes(deps) {
|
|
|
121
128
|
return json(res, 200, { skill });
|
|
122
129
|
}
|
|
123
130
|
const result = setSkillEnabled(m[1], m[2], parsed.data.enabled);
|
|
124
|
-
if ("error" in result)
|
|
131
|
+
if ("error" in result) {
|
|
132
|
+
const shipped = limixBundledSwitch(m[1], m[2], parsed.data.enabled);
|
|
133
|
+
if (shipped)
|
|
134
|
+
return json(res, shipped.status, shipped.body);
|
|
125
135
|
return json(res, 404, { error: result.error });
|
|
136
|
+
}
|
|
126
137
|
return json(res, 200, { skill: result });
|
|
127
138
|
}
|
|
128
139
|
if (m && method === "DELETE") {
|
package/package.json
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: windows-computer-automation
|
|
3
|
+
description: Điều phối AI Agent hoặc Bot điều khiển Windows trong Herionix qua Agent Desktop và Windows-MCP. Dùng khi cần đọc màn hình, thao tác cửa sổ, nhập liệu, trình duyệt, quản lý ứng dụng, file hoặc tiến trình; lựa chọn tool phù hợp, quản lý quyền, tránh bot giành chuột, xác minh kết quả và chống lặp thao tác. Hỗ trợ thiết kế hoặc khắc phục tích hợp hai MCP Server vào ứng dụng AI nhiều agent.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Windows Computer Automation — Herionix
|
|
7
|
+
|
|
8
|
+
## Mục tiêu
|
|
9
|
+
|
|
10
|
+
Dùng **Agent Desktop** cho phần tử giao diện Windows dựa trên accessibility tree; dùng **Windows-MCP** cho công cụ hệ thống, filesystem, app, process và dự phòng UI khi thích hợp. Đây là Skill hướng dẫn điều phối: **không tự cài MCP Server hoặc cấp quyền cho máy**. Chỉ thực thi khi Herionix đã kết nối thực tế với các MCP Server và tool được cấp phép.
|
|
11
|
+
|
|
12
|
+
## Quy trình bắt buộc
|
|
13
|
+
|
|
14
|
+
1. **Xác định nhiệm vụ và giới hạn:** Xác nhận ứng dụng/cửa sổ, đầu vào/đầu ra, đường dẫn file, hành động cuối cùng có được phép thực hiện không. Phân biệt chỉ đọc với thay đổi hệ thống.
|
|
15
|
+
2. **Khám phá năng lực thật:** Lấy `list_tools` của hai MCP Server; không giả định tool có mặt chỉ vì tài liệu có tên. Kiểm tra trạng thái desktop, hệ điều hành, phiên đăng nhập và quyền. Nếu thiếu connector, dừng và thông báo cụ thể; không giả vờ đã thao tác.
|
|
16
|
+
3. **Kiểm tra rủi ro:** Đọc [references/security.md](references/security.md). Những tác vụ xóa/ghi đè, chạy lệnh, sửa Registry, gửi nội dung ra ngoài, thanh toán, đổi quyền hoặc tắt tiến trình cần quyết định theo policy của host. Model **không tự phê duyệt**. Host phải chặn hành động trước khi gọi tool.
|
|
17
|
+
4. **Điều phối một desktop:** Một `desktop_id` chỉ được có **một agent giữ quyền ghi/nhập liệu** tại một thời điểm; khóa ở backend Herionix, không chỉ trong prompt. Nhiều agent có thể lập kế hoạch/đọc nếu đọc không cản trở; việc ghi phải đi qua hàng đợi.
|
|
18
|
+
5. **Quan sát trước khi thao tác:** Ưu tiên accessibility tree, ref và window ID; tránh tọa độ nếu còn lựa chọn định danh ổn định. Chọn provider theo bảng dưới đây và [references/routing.md](references/routing.md).
|
|
19
|
+
6. **Thực thi tối thiểu:** Mỗi lần chỉ thực thi bước cần thiết. Không gọi song song hai tool nhập liệu. Sử dụng key chống trùng `task_id + step_id + target + desired_effect` do backend cấp.
|
|
20
|
+
7. **Xác minh sau thao tác:** Đọc lại phần tử/trạng thái/file hoặc kết quả tương ứng, xác nhận hậu điều kiện. `tool returned success` không đồng nghĩa công việc đã hoàn thành.
|
|
21
|
+
8. **Phục hồi có giới hạn:** Với `STALE_REF`/`AMBIGUOUS_TARGET`, làm mới snapshot và định vị lại. Chỉ retry khi upstream công bố an toàn hoặc hậu điều kiện chứng minh chưa xảy ra tác dụng phụ. Dừng sau tối đa **1 retry kỹ thuật** cho một bước, hoặc **2 chu kỳ không tiến triển**; không gọi vòng lặp vô hạn, không tự `microcompact` để che lỗi.
|
|
22
|
+
9. **Báo cáo:** Trả lời ngắn bằng tiếng Việt: việc đã làm/đã xác minh, file được tạo (nếu có), bước chưa hoàn tất, các phê duyệt cần thiết, lỗi và bằng chứng không nhạy cảm. Không ghi log ảnh/chụp clipboard chứa bí mật.
|
|
23
|
+
|
|
24
|
+
## Chọn công cụ
|
|
25
|
+
|
|
26
|
+
| Tình huống | Ưu tiên | Dự phòng / điều kiện |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Đọc/nhấn/nhập trên UI có ref định danh | Agent Desktop: `desktop_snapshot`, `desktop_click`, `desktop_type`... | Windows-MCP `Snapshot` + `Click`/`Type` sau khi định vị mới |
|
|
29
|
+
| Giao diện không xuất accessibility hoặc điều khiển tọa độ | Windows-MCP `Snapshot` / `Screenshot`, `Click`, `Type` | Agent Desktop `--headed` chỉ khi cần và được phép |
|
|
30
|
+
| Khởi động, chuyển cửa sổ ứng dụng | Windows-MCP `App` | Agent Desktop `desktop_launch` / window tools nếu có |
|
|
31
|
+
| Xem, ghi file trong thư mục được cho phép | Windows-MCP `FileSystem` | Không thay bằng UI nếu có API/file-tool tin cậy |
|
|
32
|
+
| Kiểm tra tiến trình | Windows-MCP `Process` (chỉ list) | Terminate yêu cầu xác nhận từ host |
|
|
33
|
+
| PowerShell / Registry | **Mặc định không mở quyền** | Chỉ bật có chủ đích trong môi trường kiểm soát, qua host approval |
|
|
34
|
+
| Trang web cần DOM | Windows-MCP `Snapshot` (`use_dom` nếu phiên bản hỗ trợ) | Với luồng web phức tạp, cân nhắc browser automation chuyên biệt |
|
|
35
|
+
|
|
36
|
+
**Không chuyển provider để retry mù:** Sau lỗi của một thao tác có tác dụng phụ, phải quan sát và xác minh trạng thái trước khi quyết định bước tiếp theo.
|
|
37
|
+
|
|
38
|
+
## Luồng chống tốn token
|
|
39
|
+
|
|
40
|
+
- Lưu `window_id`, `snapshot_id`, mục tiêu và tóm tắt trạng thái ngắn; **vô hiệu** ref cũ sau mỗi thay đổi UI.
|
|
41
|
+
- Không gửi ảnh toàn màn hình ở mọi bước; dùng tree compact, vùng chụp nhỏ hoặc snapshot theo app.
|
|
42
|
+
- Chỉ tiếp tục khi có `progress marker` mới (trạng thái thay đổi, file xuất hiện, nút đổi trạng thái, trang chuyển); nếu hai lần không có tiến triển, dừng và báo lỗi.
|
|
43
|
+
- Nếu hai agent cùng xin điều khiển, chỉ một agent thực thi; agent còn lại chờ hoặc làm việc không tác động desktop.
|
|
44
|
+
|
|
45
|
+
## Kết nối và kiểm thử
|
|
46
|
+
|
|
47
|
+
- Tham khảo [references/herionix-integration.md](references/herionix-integration.md) để cài và kết nối hai MCP Server qua stdio.
|
|
48
|
+
- Dùng [assets/mcp-servers.example.json](assets/mcp-servers.example.json) làm cấu hình minh họa, **không coi là file cấu hình Herionix chính thức**.
|
|
49
|
+
- Có thể dùng `scripts/policy_engine.py` làm ví dụ bộ định tuyến/rào chắn **không có quyền tự thực thi OS**; backend phải thực hiện kiểm tra policy, khóa và approval thật. Ví dụ và kiểm thử: [references/scenarios.md](references/scenarios.md).
|
|
50
|
+
|
|
51
|
+
## Cách ứng xử khi không có quyền điều khiển máy
|
|
52
|
+
|
|
53
|
+
Trình bày kế hoạch, lệnh cấu hình hoặc code phù hợp; nói rõ chưa thi hành và chưa xác minh trên Windows thật. Không tuyên bố đã tích hợp vào máy hay Herionix nếu chưa có truy cập đến mã nguồn và runtime của ứng dụng.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Prompt dành cho Leader — triển khai Windows Computer Automation vào Herionix
|
|
2
|
+
|
|
3
|
+
Bạn là Leader kỹ thuật của dự án Herionix (desktop Windows). Hãy tích hợp Skill `windows-computer-automation` đính kèm vào hệ thống Bot/Teammate. Mục tiêu: mỗi bot được sử dụng có phân quyền cả Agent Desktop MCP và Windows-MCP; không có hai bot cùng tác động input trên một Windows interactive session.
|
|
4
|
+
|
|
5
|
+
## Quy tắc làm việc
|
|
6
|
+
|
|
7
|
+
1. Kiểm tra cấu trúc mã nguồn Herionix và cơ chế MCP/Tool/Skill hiện có; không đoán framework, đường dẫn hay version. Lập kế hoạch ngắn, phân task cho Teammate phù hợp, chỉ thay đổi các file cần thiết.
|
|
8
|
+
2. Đọc `SKILL.md`, `references/herionix-integration.md`, `references/security.md`, `references/routing.md` trong gói Skill; dùng file `assets/mcp-servers.example.json` như cấu hình tham khảo, không giả định schema Herionix tương thích.
|
|
9
|
+
3. Kết nối Agent Desktop (`agent-desktop mcp`) và Windows-MCP (`uvx windows-mcp serve` qua stdio). Cần kiểm tra `list_tools` động, healthcheck, startup/shutdown, timeout, lỗi transport; không spawn server trùng lặp cho từng bot dùng chung desktop.
|
|
10
|
+
4. Xây các module `ToolRegistry`, `ComputerControlManager`, `AuthorizationService`, `DesktopSessionScheduler`, `ExecutionLedger`, `Verifier` hoặc tương đương với kiến trúc dự án.
|
|
11
|
+
5. Ưu tiên Agent Desktop cho thao tác có ref accessibility; Windows-MCP cho App/FileSystem/Process và fallback UI. Quan sát trước thao tác, xác minh hậu điều kiện sau thao tác. Không retry mù sau side effect.
|
|
12
|
+
6. Bảo vệ ở backend: allowlist đường dẫn đã canonicalize trên Windows, quyền tool từng bot, approval UI do người dùng xác nhận, disable PowerShell/Registry mặc định, khóa single-writer desktop, idempotency từng step, max 1 retry kỹ thuật + 2 vòng không tiến triển. Model không được tự gửi cờ phê duyệt để vượt policy.
|
|
13
|
+
7. UI tiếng Việt trong `Tools > Windows Computer Automation`: bật/tắt providers, trạng thái kết nối, chọn thư mục cho phép, quyền Bot, hàng đợi đang giữ desktop, log tối giản, nút Dừng khẩn cấp.
|
|
14
|
+
8. Bổ sung unit/integration tests cho đúng 8 kịch bản trong `references/herionix-integration.md`; thử nghiệm khi không có GUI session, khi MCP server crash, khi ref UI stale và khi người dùng điều khiển chuột.
|
|
15
|
+
9. Không phá module Hermes, AionUI Teamate, các cấu hình Bot hoặc dữ liệu người dùng. Không expose MCP ra mạng công khai.
|
|
16
|
+
|
|
17
|
+
## Tiêu chuẩn hoàn tất
|
|
18
|
+
|
|
19
|
+
- Có code thực thi trong Herionix, không chỉ prompt hoặc file SKILL.md.
|
|
20
|
+
- Bot có thể chọn provider theo nhiệm vụ, nhưng mọi desktop input được serialize.
|
|
21
|
+
- Không có quyền ngoài allowlist; thao tác rủi ro cần approval thật từ UI host.
|
|
22
|
+
- Unit + integration tests đạt, báo cáo file thay đổi và cách chạy.
|
|
23
|
+
- Không tuyên bố đã test GUI thật nếu chỉ chạy mock tests.
|
|
24
|
+
|
|
25
|
+
Hãy bắt đầu bằng việc khám phá codebase, trình bày ngắn những module hiện có và triển khai theo từng phần kiểm chứng được. Không hỏi lại những thông tin đã thể hiện trong mã nguồn.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"available": {
|
|
3
|
+
"agent_desktop": true,
|
|
4
|
+
"windows_mcp": true
|
|
5
|
+
},
|
|
6
|
+
"allowed_write_roots": [
|
|
7
|
+
"C:\\Users\\Public\\Documents\\Herionix-Exports"
|
|
8
|
+
],
|
|
9
|
+
"system_tools_enabled": false,
|
|
10
|
+
"allow_executable_launch": false,
|
|
11
|
+
"max_retries_per_step": 1,
|
|
12
|
+
"max_no_progress_cycles": 2,
|
|
13
|
+
"require_host_confirmation_for": [
|
|
14
|
+
"delete",
|
|
15
|
+
"overwrite",
|
|
16
|
+
"send",
|
|
17
|
+
"upload",
|
|
18
|
+
"purchase",
|
|
19
|
+
"security_change",
|
|
20
|
+
"terminate_process"
|
|
21
|
+
],
|
|
22
|
+
"allowed_read_roots": [
|
|
23
|
+
"C:\\Users\\Public\\Documents\\Herionix-Exports"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"agent-desktop": {
|
|
4
|
+
"command": "agent-desktop",
|
|
5
|
+
"args": ["mcp"]
|
|
6
|
+
},
|
|
7
|
+
"windows-mcp": {
|
|
8
|
+
"command": "uvx",
|
|
9
|
+
"args": [
|
|
10
|
+
"windows-mcp",
|
|
11
|
+
"serve",
|
|
12
|
+
"--exclude-tools",
|
|
13
|
+
"PowerShell,Registry",
|
|
14
|
+
"--control-policy",
|
|
15
|
+
"balanced"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "windows-computer-automation",
|
|
3
|
+
"name": "Windows Computer Automation",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Điều khiển Windows an toàn: đọc màn hình, thao tác cửa sổ, nhập liệu, trình duyệt, ứng dụng, tệp và tiến trình qua Agent Desktop và Windows-MCP; chọn đúng công cụ, xác minh kết quả, chống lặp thao tác.",
|
|
6
|
+
"defaultEnabled": true,
|
|
7
|
+
"triggerTerms": [
|
|
8
|
+
"windows-computer-automation",
|
|
9
|
+
"điều khiển máy tính",
|
|
10
|
+
"điều khiển windows",
|
|
11
|
+
"thao tác trên máy",
|
|
12
|
+
"thao tác máy tính",
|
|
13
|
+
"tự động hóa windows",
|
|
14
|
+
"mở ứng dụng",
|
|
15
|
+
"chụp màn hình",
|
|
16
|
+
"computer use",
|
|
17
|
+
"windows-mcp",
|
|
18
|
+
"agent desktop",
|
|
19
|
+
"control my computer",
|
|
20
|
+
"automate windows"
|
|
21
|
+
],
|
|
22
|
+
"requiredCapabilities": [],
|
|
23
|
+
"source": {
|
|
24
|
+
"repo": "leomieo (Limix AI)",
|
|
25
|
+
"license": "Proprietary (Limix AI)"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Hướng dẫn tích hợp vào Herionix (Windows 10/11)
|
|
2
|
+
|
|
3
|
+
## 1. Chuẩn bị công cụ
|
|
4
|
+
|
|
5
|
+
Cài Node.js/npm và Agent Desktop:
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
npm install -g agent-desktop
|
|
9
|
+
agent-desktop status
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Cài `uv` và Python phiên bản phù hợp theo trang Windows-MCP (README hiện nêu Python 3.14+). Với Windows-MCP, dùng lệnh sau để kiểm tra server và lưu ý nó sẽ tiếp tục chạy cho đến khi client/terminal đóng:
|
|
13
|
+
|
|
14
|
+
```powershell
|
|
15
|
+
uvx windows-mcp serve --exclude-tools "PowerShell,Registry" --control-policy balanced
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Tài liệu nguồn:
|
|
19
|
+
- https://github.com/vercel-labs/agent-desktop
|
|
20
|
+
- https://github.com/CursorTouch/Windows-MCP
|
|
21
|
+
|
|
22
|
+
## 2. Đăng ký hai MCP Server vào backend
|
|
23
|
+
|
|
24
|
+
Xem `assets/mcp-servers.example.json`. Đây là **MCP client config mẫu**, không phải định dạng config Herionix đã được xác nhận. Nếu Herionix dùng định dạng khác, chuyển hai `command` + `args` tương ứng vào cấu hình app. Chạy server trong phiên người dùng đang đăng nhập Windows và dùng stdio local.
|
|
25
|
+
|
|
26
|
+
Backend phải hỗ trợ startup/healthcheck, khám phá `list_tools`, chuẩn hóa lỗi, timeout, shutdown; không spawn bản sao MCP server cho từng bot nếu chúng cùng điều khiển một session. Nếu máy có nhiều interactive sessions, phân biệt bằng `desktop_id` thực tế.
|
|
27
|
+
|
|
28
|
+
## 3. Gắn Skill
|
|
29
|
+
|
|
30
|
+
Đặt thư mục `windows-computer-automation` vào **skills directory mà Herionix thực sự tải**. Gắn Skill cho các Bot/Teammate được cho quyền computer-use. Nếu Herionix chưa có skill loader, cần thêm parser `SKILL.md` và entrypoints tương ứng — chỉ sao chép thư mục chưa biến thành kết nối MCP.
|
|
31
|
+
|
|
32
|
+
## 4. Component đề xuất
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
Bot / Teammate
|
|
36
|
+
| task + intent
|
|
37
|
+
v
|
|
38
|
+
ComputerControlManager (orchestrator)
|
|
39
|
+
|-- policy + host approvals
|
|
40
|
+
|-- desktop mutex + task queue
|
|
41
|
+
|-- state snapshots + idempotency
|
|
42
|
+
|-- progress watchdog + verification
|
|
43
|
+
+-- AgentDesktopAdapter ------> agent-desktop MCP
|
|
44
|
+
+-- WindowsMcpAdapter --------> windows-mcp MCP
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Các module và trách nhiệm:
|
|
48
|
+
- `ToolRegistry`: khám phá tên, schema và trạng thái tool của hai server.
|
|
49
|
+
- `Planner/Router`: chọn provider theo tác vụ, không tự tạo lại tác dụng phụ khi chuyển provider.
|
|
50
|
+
- `AuthorizationService`: người dùng cấp quyền, allowlist path, risk classification và token một lần.
|
|
51
|
+
- `DesktopSessionScheduler`: khóa input per desktop/session và hàng đợi công bằng.
|
|
52
|
+
- `ExecutionLedger`: task ID, step ID, state digest, trạng thái hoàn thành, idempotency.
|
|
53
|
+
- `Verifier`: hậu điều kiện sau thao tác; phản hồi lỗi có phân loại để tránh vòng lặp.
|
|
54
|
+
|
|
55
|
+
## 5. Cấu hình UI khuyến nghị
|
|
56
|
+
|
|
57
|
+
Trong `Herionix > Tools > Windows Computer Automation`: bật/tắt cả hai providers; ưu tiên Agent Desktop cho UI; hiển thị số bot đang chờ quyền desktop; cho phép chọn thư mục ghi; quyền admin tools mặc định off; số lần retry tối đa 1; giới hạn 2 vòng không tiến triển; nút Stop/Emergency Release; audit log đã che dữ liệu nhạy cảm.
|
|
58
|
+
|
|
59
|
+
## 6. Kiểm thử chấp nhận
|
|
60
|
+
|
|
61
|
+
1. Bot A nhấn nút trong Notepad bằng ref; Bot B chờ, không can thiệp bàn phím.
|
|
62
|
+
2. Ref UI hết hạn → snapshot mới; không dùng lại ref cũ và không spam retry.
|
|
63
|
+
3. Bot liệt kê file đọc-only; ghi `.txt` trong thư mục cho phép và xác minh file.
|
|
64
|
+
4. Xóa file / chạy PowerShell / Registry bị block hoặc yêu cầu host approval tương ứng.
|
|
65
|
+
5. MCP Server tắt giữa tác vụ → dừng có trạng thái rõ, không tự lặp thao tác.
|
|
66
|
+
6. Cùng một `step_id` gửi hai lần → tác dụng phụ chỉ thực thi tối đa một lần ở host.
|
|
67
|
+
7. No progress hai chu kỳ → dừng và báo lý do, không clear context để lặp vô hạn.
|
|
68
|
+
8. Không có desktop session/unlock → báo bị chặn, không giả thành công.
|
|
69
|
+
|
|
70
|
+
## 7. Phạm vi bản đóng gói
|
|
71
|
+
|
|
72
|
+
Skill này bao gồm quy trình, policy router có thể kiểm thử và cấu hình MCP mẫu. **Chưa phải bản cài đặt Herionix hoặc proxy MCP chạy sẵn**; thực thi connector, mutex liên tiến trình, duyệt quyền và xác minh file thật phải được hiện thực trong Herionix backend.
|