@mogragsigma/dsh-dual-host-plugin 0.33.3 → 2.0.0
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 +68 -97
- package/bundle/README.md +68 -97
- package/bundle/SETUP.md +87 -0
- package/bundle/approval-chat.js +2 -2
- package/bundle/approval-feishu.js +4 -1
- package/bundle/approval-ledger.js +30 -13
- package/bundle/approval-workbench.js +2 -0
- package/bundle/artifact-media.js +2 -2
- package/bundle/compatibility.json +47 -1
- package/bundle/configuration-v2.json +15 -0
- package/bundle/delivery-state.js +2 -0
- package/bundle/development-gate.js +2 -0
- package/bundle/distribution-lock.json +18 -18
- package/bundle/gateway-maintenance.js +1 -0
- package/bundle/global-rules/AGENTS.md +24 -20
- package/bundle/index.js +135 -25
- package/bundle/intervention-state.js +1 -1
- package/bundle/link-contract.js +9 -4
- package/bundle/managed-closeout.js +45 -0
- package/bundle/managed-cloud-policy.js +19 -0
- package/bundle/managed-command.js +71 -0
- package/bundle/managed-development-contract.js +43 -0
- package/bundle/managed-development.js +86 -0
- package/bundle/managed-endpoint.js +282 -0
- package/bundle/managed-execution.js +207 -0
- package/bundle/managed-policy.js +20 -0
- package/bundle/managed-runtime.js +186 -0
- package/bundle/managed-search.js +57 -0
- package/bundle/managed-tools.js +17 -0
- package/bundle/managed-triggers.js +85 -0
- package/bundle/native-context.js +4 -2
- package/bundle/native-entry.js +6 -2
- package/bundle/native-human-context.js +29 -0
- package/bundle/native-model-choice.js +12 -0
- package/bundle/native-rules/continuity.md +13 -0
- package/bundle/native-rules/execution.md +36 -0
- package/bundle/native-tools.js +52 -19
- package/bundle/notification-artifacts.js +26 -10
- package/bundle/notification-endpoint.js +1 -1
- package/bundle/notification-feishu.js +2 -2
- package/bundle/notification-media-endpoint.js +4 -3
- package/bundle/notification-observations.js +45 -0
- package/bundle/onboarding-cloud-package.js +1 -1
- package/bundle/onboarding-cloud.js +1 -1
- package/bundle/onboarding-file-contract.js +3 -2
- package/bundle/onboarding-file-transfer.js +4 -4
- package/bundle/onboarding-native-probe.js +1 -1
- package/bundle/onboarding-runtime-activation.js +1 -1
- package/bundle/onboarding-runtime-extension.js +2 -2
- package/bundle/package-lock.json +18 -18
- package/bundle/package.json +16 -14
- package/bundle/peer-file-review.js +1 -1
- package/bundle/peer-files.js +8 -4
- package/bundle/recovery.js +1 -1
- package/bundle/repository-gitee.js +12 -0
- package/bundle/repository-managed.js +1 -0
- package/bundle/runtime-im-lock.json +3409 -2849
- package/bundle/runtime-im-package.json +5 -5
- package/bundle/runtime-lock.json +3409 -2849
- package/bundle/runtime-package.json +5 -5
- package/bundle/schedule-ledger.js +35 -0
- package/bundle/search-contract.js +11 -1
- package/bundle/skills/task-approval/SKILL.md +20 -1
- package/bundle/skills/task-development/SKILL.md +30 -13
- package/bundle/skills/task-direct-link/SKILL.md +39 -13
- package/bundle/skills/task-file-transfer/SKILL.md +2 -2
- package/bundle/skills/task-notify/SKILL.md +2 -0
- package/bundle/skills/task-repository/SKILL.md +10 -1
- package/bundle/skills/task-research/SKILL.md +17 -2
- package/bundle/source-assessment.js +38 -0
- package/bundle/source-gitee.js +52 -0
- package/bundle/source-intake.js +137 -0
- package/bundle/source-policy-envelope.js +29 -0
- package/bundle/source-routing-tools.js +12 -0
- package/bundle/source-routing.js +218 -0
- package/bundle/source-search.js +2 -1
- package/bundle/store.js +6 -5
- package/bundle/task-artifacts.js +51 -12
- package/bundle/task-board.js +8 -4
- package/bundle/task-catalog.js +4 -3
- package/bundle/task-context.js +2 -2
- package/bundle/task-continuity-tools.js +14 -0
- package/bundle/task-continuity.js +248 -0
- package/bundle/task-envelope.js +31 -20
- package/bundle/task-notifications.js +6 -2
- package/bundle/task-observer-model.js +25 -0
- package/bundle/task-observer.js +120 -0
- package/bundle/task-results.js +1 -1
- package/bundle/task-scheduler.js +3 -3
- package/bundle/task-trigger-time.js +17 -12
- package/bundle/task-triggers.js +5 -30
- package/bundle/task-worker.js +10 -2
- package/bundle/task-workflows.js +2 -1
- package/bundle/workbench-client.js +165 -30
- package/bundle/workbench-decisions.js +22 -5
- package/bundle/worker-tools.js +8 -1
- package/compatibility.json +47 -1
- package/package.json +13 -13
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {ManagedTriggers} from './managed-triggers.js';
|
|
2
|
+
import {CLOUD_TASK_POLICY,autonomous} from './managed-cloud-policy.js';
|
|
3
|
+
import {parseEnvelope} from './task-envelope.js';
|
|
4
|
+
import {developmentContract,legacyResourceBusy} from './managed-development-contract.js';
|
|
5
|
+
import {repositoryResources} from './managed-development.js';
|
|
6
|
+
// One cloud authority for native long-running work. Legacy local records are retained, not dispatched.
|
|
7
|
+
// Existing authenticated DirectLink transports this protocol. IM is not an authority.
|
|
8
|
+
import {ManagedSlots,ManagedBudget} from './managed-runtime.js';
|
|
9
|
+
import {tx} from './recovery.js';
|
|
10
|
+
import {isAbsolute,normalize} from 'node:path';
|
|
11
|
+
const json=JSON.stringify;
|
|
12
|
+
const atomic=(db,fn)=>db.isTransaction?fn():tx(db,fn);
|
|
13
|
+
const fail=s=>{throw Error('TASK_EXECUTION_'+s);};
|
|
14
|
+
export function executionSpec(x){
|
|
15
|
+
if(!x||Object.keys(x).some(k=>!['execution_policy','end_at','schedule','role','cwd','max_requests','active_timeout_ms','period_seconds','search','commands','development'].includes(k))||!['local','cloud'].includes(x.role)||typeof x.cwd!=='string'||!isAbsolute(x.cwd)||normalize(x.cwd)!==x.cwd||x.cwd.length>1000||x.cwd.includes('\0'))fail('SPEC_INVALID');
|
|
16
|
+
const free=autonomous(x);if(x.schedule!==undefined&&(!free||x.period_seconds!==undefined||!x.schedule||typeof x.schedule!=='object'||Array.isArray(x.schedule)))fail('SCHEDULE_INVALID');if(x.execution_policy!==undefined&&!free||free&&x.role!=='cloud')fail('CLOUD_ONLY');
|
|
17
|
+
if(free&&(x.max_requests!==undefined||x.active_timeout_ms!==undefined||x.commands!==undefined))fail('RETIRED_LIMIT');
|
|
18
|
+
if(free&&x.end_at!==undefined&&(!Number.isSafeInteger(x.end_at)||x.end_at<=0))fail('LIMIT_INVALID');
|
|
19
|
+
if(!free&&(!Number.isInteger(x.max_requests)||x.max_requests<1||x.max_requests>100000||!Number.isInteger(x.active_timeout_ms)||x.active_timeout_ms<1000||x.active_timeout_ms>86400000)||x.period_seconds!==undefined&&(!Number.isInteger(x.period_seconds)||x.period_seconds<60||x.period_seconds>366*86400))fail('LIMIT_INVALID');
|
|
20
|
+
if(x.commands!==undefined&&(!Array.isArray(x.commands)||x.role!=='local'||x.commands.length<1||x.commands.length>8||new Set(x.commands).size!==x.commands.length||x.commands.some(n=>typeof n!=='string'||!/^[a-z][a-z0-9_-]{0,39}$/.test(n))))fail('COMMAND_CONTRACT_INVALID');
|
|
21
|
+
if(x.search!==undefined){const s=x.search,p=s?.model_policy;if(!s||Object.keys(s).some(k=>!['scope','model_policy','max_requests'].includes(k))||typeof s.scope!=='string'||!s.scope.trim()||s.scope.length>1000||!p||Object.keys(p).some(k=>!['provider','model','reasoningEffort'].includes(k))||p.provider!=='deepseek-official'||typeof p.model!=='string'||!p.model.trim()||p.model.length>200||p.reasoningEffort!=='disabled'||(!free&&(!Number.isSafeInteger(s.max_requests)||s.max_requests<1||s.max_requests>x.max_requests))||free&&s.max_requests!==undefined)fail('SEARCH_CONTRACT_INVALID');}
|
|
22
|
+
return {role:x.role,cwd:x.cwd,...(free?{execution_policy:CLOUD_TASK_POLICY,...(x.end_at!==undefined?{end_at:x.end_at}:{})}:{max_requests:x.max_requests,active_timeout_ms:x.active_timeout_ms}),...(x.schedule?{schedule:x.schedule}:{}),...(x.period_seconds?{period_seconds:x.period_seconds}:{}),...(x.search?{search:x.search}:{}),...(x.commands?{commands:x.commands}:{}),...(x.development!==undefined?{development:developmentContract(x.development,x.role)}:{})};
|
|
23
|
+
}
|
|
24
|
+
export class ManagedExecution {
|
|
25
|
+
constructor(g){
|
|
26
|
+
this.g=g;this.db=g.store.db;this.now=g.now;this.slots=new ManagedSlots(this.db,this.now,{localEnabled:false});this.budgets=new ManagedBudget(this.db,this.now);
|
|
27
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS managed_development_evidence(task_id TEXT PRIMARY KEY,revision INTEGER NOT NULL,record TEXT NOT NULL); CREATE TABLE IF NOT EXISTS managed_executions(task_id TEXT PRIMARY KEY,approval_id TEXT NOT NULL,
|
|
28
|
+
revision INTEGER NOT NULL,role TEXT NOT NULL,spec TEXT NOT NULL,state TEXT NOT NULL,due INTEGER NOT NULL,
|
|
29
|
+
human INTEGER NOT NULL DEFAULT 0,claim TEXT,instance TEXT,started INTEGER,lease_until INTEGER,
|
|
30
|
+
session_id TEXT,report TEXT,error TEXT,parked_state TEXT,runs INTEGER NOT NULL DEFAULT 0,input_cursor INTEGER NOT NULL DEFAULT 0);
|
|
31
|
+
CREATE TABLE IF NOT EXISTS managed_execution_receipts(claim TEXT PRIMARY KEY,payload TEXT NOT NULL,result TEXT NOT NULL);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS managed_unstarted_reclaims(claim TEXT PRIMARY KEY,task_id TEXT NOT NULL,instance TEXT NOT NULL,role TEXT NOT NULL);`);
|
|
33
|
+
if(!this.db.prepare('PRAGMA table_info(managed_executions)').all().some(x=>x.name==='authority'))this.db.exec('ALTER TABLE managed_executions ADD COLUMN authority TEXT');
|
|
34
|
+
if(!this.db.prepare('PRAGMA table_info(managed_executions)').all().some(x=>x.name==='occurrence_id'))this.db.exec('ALTER TABLE managed_executions ADD COLUMN occurrence_id TEXT');
|
|
35
|
+
this.schedules=new ManagedTriggers(this);
|
|
36
|
+
}
|
|
37
|
+
observed(id,baseline){try{this.g.observer?.changed(id,baseline);}catch{} /* Never block execution for observation failure. */}
|
|
38
|
+
get(id){return this.db.prepare('SELECT * FROM managed_executions WHERE task_id=?').get(id);}
|
|
39
|
+
busy(id){return !!this.db.prepare('SELECT 1 FROM managed_slots WHERE task_id=?').get(id);}
|
|
40
|
+
owns(id){return !!this.get(id);}
|
|
41
|
+
authority(r,{resuming=false}={}){
|
|
42
|
+
if(!r)fail('NOT_FOUND');if(!resuming)this.g.continuity.assertActive(r.task_id);
|
|
43
|
+
if(autonomous(JSON.parse(r.spec))){
|
|
44
|
+
if(!r.authority||this.g.catalog.head(r.task_id).spec_revision!==r.revision)fail('AUTHORITY_CHANGED');
|
|
45
|
+
const spec=JSON.parse(r.spec);if(spec.end_at&&this.now()>=spec.end_at)fail('BUSINESS_DEADLINE');
|
|
46
|
+
this.g.sourceRouting?.assertTask(r.task_id,{required:JSON.parse(r.authority).kind==='proactive-policy'});
|
|
47
|
+
return JSON.parse(r.authority);
|
|
48
|
+
}
|
|
49
|
+
const a=this.g.approvals.view(r.approval_id);
|
|
50
|
+
if(a.state!=='approved'||a.spec_revision!==r.revision||json(a.proposal.execution)!==r.spec)fail('AUTHORITY_CHANGED');
|
|
51
|
+
return a;
|
|
52
|
+
}
|
|
53
|
+
view(id){const r=this.get(id);if(!r)return null;return {task_id:id,role:r.role,state:r.state,due:r.due,report:r.report?JSON.parse(r.report):null,error:r.error,runs:r.runs,session_id:r.session_id,development:this.evidenceView(id),budget:this.budgets.view(id),filesystem:r.role==='cloud'?'host-account':'approved-local-workspace',slot_held:this.busy(id),schedules:this.schedules.list(id)};}
|
|
54
|
+
evidenceView(id){const r=this.db.prepare('SELECT * FROM managed_development_evidence WHERE task_id=?').get(id);return r?{...JSON.parse(r.record),task_revision:r.revision,historical:r.revision!==this.g.catalog.head(id).spec_revision||JSON.parse(r.record).approval_id!==this.get(id)?.approval_id,checked_now:false}:null;}
|
|
55
|
+
evidence(role,a){this.validate(role,a);const r=this.owned(role,a);if(!JSON.parse(r.spec).development||!a.evidence||a.evidence.schema!==1||Buffer.byteLength(json(a.evidence))>131072)fail('EVIDENCE_INVALID');this.db.prepare('INSERT OR REPLACE INTO managed_development_evidence VALUES (?,?,?)').run(r.task_id,r.revision,json({...a.evidence,approval_id:r.approval_id}));return {saved:true};}
|
|
56
|
+
// Direct, durable user-task authority; approval_id='' is only a schema-compatible
|
|
57
|
+
// absence marker. There is no synthetic approval or credential row.
|
|
58
|
+
start({task_id,expected_revision,spec,policy}){
|
|
59
|
+
const h=this.g.catalog.head(task_id),e=parseEnvelope(h.goal);
|
|
60
|
+
if(!autonomous(e)||!this.g.continuity?.managed(task_id))fail('NATIVE_TASK_REQUIRED');
|
|
61
|
+
if(h.spec_revision!==expected_revision)fail('REVISION_CHANGED');
|
|
62
|
+
this.g.continuity.assertActive(task_id);spec=executionSpec(spec);
|
|
63
|
+
if(!autonomous(spec)||spec.role!=='cloud')fail('CLOUD_ONLY');
|
|
64
|
+
const old=this.get(task_id);
|
|
65
|
+
const existingAuthority=old?.authority?JSON.parse(old.authority):null;
|
|
66
|
+
if(e.origin.kind==='policy'){if(!this.g.sourceRouting)fail('AUTHORITY_CHANGED');this.g.sourceRouting.assertTask(task_id,{required:true});}
|
|
67
|
+
const authority=json({kind:e.origin.kind==='policy'&&existingAuthority?.kind!=='direct-user-task'?'proactive-policy':'direct-user-task',model_policy:policy,origin:e.origin,...(existingAuthority?.adoption?{adoption:existingAuthority.adoption}:{})});
|
|
68
|
+
if(old){
|
|
69
|
+
if(!autonomous(JSON.parse(old.spec)))fail('LEGACY_CONTRACT_RETAINED');
|
|
70
|
+
if(old.revision===expected_revision&&old.spec===json(spec)&&old.authority===authority)return this.view(task_id);
|
|
71
|
+
if(old.role!==spec.role||JSON.parse(old.spec).cwd!==spec.cwd)fail('SESSION_BINDING_CHANGED');
|
|
72
|
+
}
|
|
73
|
+
this.g.assertTaskQuiescent(task_id);
|
|
74
|
+
if(this.schedules.current(task_id)&&this.schedules.pending(this.schedules.current(task_id)))fail('SCHEDULE_OCCURRENCE_UNSETTLED');
|
|
75
|
+
const plan=this.schedules.prepare(task_id,spec,old);
|
|
76
|
+
atomic(this.db,()=>{
|
|
77
|
+
this.budgets.bind(task_id,policy,{executionPolicy:CLOUD_TASK_POLICY,endAt:spec.end_at,search:spec.search},{amend:!!old});
|
|
78
|
+
if(old)this.db.prepare("UPDATE managed_executions SET revision=?,spec=?,authority=?,state='queued',due=?,error=NULL,parked_state=NULL WHERE task_id=?").run(expected_revision,json(spec),authority,this.now(),task_id);
|
|
79
|
+
else this.db.prepare("INSERT INTO managed_executions(task_id,approval_id,revision,role,spec,authority,state,due) VALUES (?,'',?,'cloud',?,?,'queued',?)").run(task_id,expected_revision,json(spec),authority,this.now());
|
|
80
|
+
this.schedules.bind(task_id,spec,plan);if(plan)this.db.prepare("UPDATE managed_executions SET state='scheduled',human=0 WHERE task_id=?").run(task_id);
|
|
81
|
+
});this.observed(task_id,old?.runs??0);return this.view(task_id);
|
|
82
|
+
}
|
|
83
|
+
enqueue({task_id,approval_id}){
|
|
84
|
+
if(!this.g.continuity?.managed(task_id))fail('NATIVE_TASK_REQUIRED');
|
|
85
|
+
const existing=this.get(task_id);if(autonomous(parseEnvelope(this.g.catalog.head(task_id).goal)))fail('DIRECT_EXECUTION_REQUIRED');
|
|
86
|
+
if(existing?.approval_id===approval_id)return this.view(task_id);
|
|
87
|
+
this.g.continuity.assertActive(task_id);this.g.assertTaskQuiescent(task_id);
|
|
88
|
+
const a=this.g.approvals.view(approval_id),e=a.proposal.execution;
|
|
89
|
+
if(a.task_id!==task_id||a.state!=='approved'||!e||a.proposal.scopes.join()!=='develop')fail('APPROVAL_REQUIRED');
|
|
90
|
+
if(existing&&(existing.role!==e.role||JSON.parse(existing.spec).cwd!==e.cwd))fail('SESSION_BINDING_CHANGED');
|
|
91
|
+
tx(this.db,()=>{
|
|
92
|
+
this.budgets.bind(task_id,a.proposal.model_policy,{maxRequests:e.max_requests,endAt:a.expires,search:e.search},{amend:!!existing});
|
|
93
|
+
if(existing)this.db.prepare("UPDATE managed_executions SET approval_id=?,revision=?,spec=?,state='queued',due=?,human=0,error=NULL,parked_state=NULL WHERE task_id=?").run(approval_id,a.spec_revision,json(e),this.now(),task_id);
|
|
94
|
+
else this.db.prepare("INSERT INTO managed_executions(task_id,approval_id,revision,role,spec,state,due) VALUES (?,?,?,?,?,'queued',?)").run(task_id,approval_id,a.spec_revision,e.role,json(e),this.now());
|
|
95
|
+
});this.observed(task_id,existing?.runs??0);return this.view(task_id);
|
|
96
|
+
}
|
|
97
|
+
// Only persisted, direct human follow-input can earn the +1. New tasks never do.
|
|
98
|
+
follow(id){
|
|
99
|
+
const r=this.get(id);if(r&&JSON.parse(r.spec).schedule&&!this.schedules.followAllowed(r))return;if(!r||!['idle','waiting','waiting_input','queued'].includes(r.state)||!r.runs||!this.g.continuity.canRun(id))return;
|
|
100
|
+
const pending=this.db.prepare("SELECT count(*) n,max(CASE WHEN json_extract(e.source,'$.kind') IS NULL THEN 1 ELSE 0 END) human FROM task_inputs i JOIN task_input_events e ON i.event_id=e.id WHERE i.task_id=? AND i.mode='follow' AND i.state='saved' AND i.seq>?").get(id,r.input_cursor);
|
|
101
|
+
if(pending.n)this.db.prepare("UPDATE managed_executions SET state='queued',human=?,due=? WHERE task_id=?").run(pending.human?1:0,this.now(),id);
|
|
102
|
+
}
|
|
103
|
+
claim(role,{instance_id,development_contract,autonomy_contract}){
|
|
104
|
+
if(role==='local')return {item:null,reason:'local-managed-execution-retired'};
|
|
105
|
+
if(typeof instance_id!=='string'||!instance_id||instance_id.length>100)fail('INSTANCE_INVALID');
|
|
106
|
+
return tx(this.db,()=>{
|
|
107
|
+
this.reconcile();
|
|
108
|
+
const ready=this.db.prepare("SELECT * FROM managed_executions WHERE role=? AND state IN ('queued','waiting') AND due<=? ORDER BY human DESC,due,task_id").all(role,this.now());
|
|
109
|
+
for(const r of ready){
|
|
110
|
+
if(autonomous(JSON.parse(r.spec))&&autonomy_contract!==1)continue;
|
|
111
|
+
if(JSON.parse(r.spec).development&&development_contract!==1)continue;
|
|
112
|
+
if(!this.schedules.canClaim(r))continue;
|
|
113
|
+
try{this.authority(r);}catch(e){this.db.prepare("UPDATE managed_executions SET error=? WHERE task_id=?").run(e.message,r.task_id);continue;}
|
|
114
|
+
// Until finer path-conflict evidence exists, one explicit working directory is
|
|
115
|
+
// a cross-host resource key. It is a scheduling key, NOT a cloud file sandbox.
|
|
116
|
+
const spec=JSON.parse(r.spec);let resources=['cwd:'+spec.cwd];
|
|
117
|
+
if(spec.development){if(role==='cloud'){try{resources.push(...repositoryResources(spec.cwd,spec.development));}catch(e){if(spec.development.branch){this.db.prepare('UPDATE managed_executions SET error=? WHERE task_id=?').run(e.message,r.task_id);continue;}}}else if(role==='local')resources.push('local-managed-development');}
|
|
118
|
+
if(legacyResourceBusy(this.db,resources))continue;
|
|
119
|
+
const slot=this.slots.reserve({task_id:r.task_id,role,resources,humanFollowup:!!r.human});
|
|
120
|
+
if(!slot.reserved)continue;
|
|
121
|
+
this.db.prepare("UPDATE managed_executions SET state='claimed',claim=?,instance=?,lease_until=?,error=NULL WHERE task_id=?").run(slot.claim,instance_id,this.now()+30000,r.task_id);
|
|
122
|
+
return {item:this.item(this.get(r.task_id))};
|
|
123
|
+
}return {item:null};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
item(r){const a=r.authority?JSON.parse(r.authority):this.g.approvals.view(r.approval_id);return {...r,spec:JSON.parse(r.spec),policy:a.model_policy??a.proposal.model_policy,goal:this.g.catalog.head(r.task_id).goal,task_scope:r.authority?parseEnvelope(this.g.catalog.head(r.task_id).goal).entry_interpretation:a.proposal.changes,budget:this.budgets.view(r.task_id),trigger:this.schedules.context(r.occurrence_id)};}
|
|
127
|
+
owned(role,a){const r=this.get(a.task_id);if(!r||r.role!==role||r.claim!==a.claim||r.instance!==a.instance_id)fail('CLAIM_DENIED');this.slots.assert(r.task_id,r.claim);return r;}
|
|
128
|
+
validate(role,a){
|
|
129
|
+
const r=this.owned(role,a);this.authority(r);
|
|
130
|
+
if(!['claimed','running'].includes(r.state)||r.lease_until<=this.now())fail('LEASE_ENDED');
|
|
131
|
+
if(r.started&&JSON.parse(r.spec).active_timeout_ms&&this.now()>=r.started+JSON.parse(r.spec).active_timeout_ms)fail('ACTIVE_LIMIT');
|
|
132
|
+
this.db.prepare('UPDATE managed_executions SET lease_until=? WHERE task_id=?').run(this.now()+30000,r.task_id);
|
|
133
|
+
try{this.g.observer?.capture(r,a.observation);}catch{} // Observability must not interrupt valid execution.
|
|
134
|
+
return {valid:true,lease_ms:30000,control_version:this.g.continuity.state(r.task_id).version,task_id:r.task_id,input_cursor:r.input_cursor,pending_input:this.db.prepare("SELECT max(seq) seq FROM task_inputs WHERE task_id=? AND mode='follow' AND state='saved'").get(r.task_id).seq??null};
|
|
135
|
+
}
|
|
136
|
+
begin(role,a){
|
|
137
|
+
this.validate(role,a);const r=this.owned(role,a);
|
|
138
|
+
if(r.state==='running')return {item:this.item(r),already_started:true};
|
|
139
|
+
this.db.prepare("UPDATE managed_executions SET state='running',started=?,human=0,runs=runs+1 WHERE task_id=?").run(this.now(),r.task_id);
|
|
140
|
+
return {item:this.item(this.get(r.task_id)),already_started:false};
|
|
141
|
+
}
|
|
142
|
+
reserve(role,a){this.validate(role,a);const r=this.owned(role,a);if(r.state!=='running')fail('NOT_RUNNING');if(a.channel!==undefined&&a.channel!=='search')fail('BUDGET_CHANNEL_INVALID');return this.budgets.reserve(r.task_id,a.policy,a.purpose,{search:a.channel==='search'});}
|
|
143
|
+
usage(role,a){this.owned(role,a);return this.budgets.usage(a.task_id,a.request_id,a.usage);}
|
|
144
|
+
inputs(role,a){this.validate(role,a);const r=this.owned(role,a);if(r.authority&&(a.after??0)>r.input_cursor)fail('INPUT_CURSOR_GAP');const result=this.g.continuity.workerRead({task_id:r.task_id,run_id:r.claim,goal:this.g.catalog.head(r.task_id).goal},{after:a.after??0,limit:20});
|
|
145
|
+
if(result.items.length)this.db.prepare('UPDATE managed_executions SET input_cursor=max(input_cursor,?) WHERE task_id=?').run(result.items.at(-1).seq,r.task_id);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
finish(role,a){
|
|
149
|
+
const payload=json(a),old=this.db.prepare('SELECT * FROM managed_execution_receipts WHERE claim=?').get(a.claim);
|
|
150
|
+
if(old){if(this.get(a.task_id)?.role!==role)fail('CLAIM_DENIED');if(old.payload!==payload)fail('RECEIPT_CONFLICT');return JSON.parse(old.result);}
|
|
151
|
+
return tx(this.db,()=>{
|
|
152
|
+
const reclaimed=this.db.prepare('SELECT * FROM managed_unstarted_reclaims WHERE claim=?').get(a.claim);
|
|
153
|
+
if(reclaimed){
|
|
154
|
+
if(reclaimed.role!==role||reclaimed.task_id!==a.task_id||reclaimed.instance!==a.instance_id)fail('CLAIM_DENIED');
|
|
155
|
+
if(a.drained!==true||a.outcome!=='interrupted')fail('REPORT_INVALID');
|
|
156
|
+
const result={recorded:true,state:'reclaimed_before_start',task_id:a.task_id};
|
|
157
|
+
this.db.prepare('INSERT INTO managed_execution_receipts VALUES (?,?,?)').run(a.claim,payload,json(result));return result;
|
|
158
|
+
}
|
|
159
|
+
const r=this.owned(role,a);if(a.drained!==true)fail('DRAIN_REQUIRED');
|
|
160
|
+
if(!(typeof a.session_id==='string'&&a.session_id.startsWith('managed-')||a.session_id===null&&['failed','interrupted'].includes(a.outcome)&&!r.session_id)||typeof a.summary!=='string'||!a.summary||a.summary.length>4000||!['ready','waiting','failed','stopped','interrupted'].includes(a.outcome))fail('REPORT_INVALID');
|
|
161
|
+
if(a.error!==undefined&&(typeof a.error!=='string'||!/^([A-Z][A-Z_]{1,100})$/.test(a.error)))fail('REPORT_INVALID');
|
|
162
|
+
if(r.session_id&&r.session_id!==a.session_id)fail('SESSION_CHANGED');
|
|
163
|
+
let valid=true;try{this.authority(r);}catch{valid=false;}
|
|
164
|
+
const spec=JSON.parse(r.spec);if(valid&&a.outcome==='ready'&&autonomous(spec))this.assertCompletion(r,a.completion);
|
|
165
|
+
if(valid&&a.outcome==='ready'&&spec.development){const evidence=this.evidenceView(r.task_id),expected={workspace:'workspace_ready',commit:'commit_verified',push:'remote_commit_verified',pr:'pr_verified'}[spec.development.delivery];if(!evidence||evidence.historical||evidence.at<r.started||evidence.delivery?.mode!==spec.development.delivery||evidence.delivery?.state!==expected||evidence.checks?.some(c=>c.state!=='passed'))fail('DEVELOPMENT_EVIDENCE_REQUIRED');}
|
|
166
|
+
const state=!valid?'fenced':a.outcome==='interrupted'?(spec.period_seconds?'waiting':'queued'):a.outcome==='failed'?'needs_review':a.outcome==='stopped'?'needs_review':a.outcome==='waiting'?'waiting_input':spec.schedule&&r.occurrence_id?'scheduled':spec.period_seconds?'waiting':'idle';
|
|
167
|
+
// A late completion is retained, never used to revive stopped/revoked work.
|
|
168
|
+
this.db.prepare('UPDATE managed_executions SET state=?,due=?,session_id=?,report=?,error=?,claim=NULL,instance=NULL,lease_until=NULL,started=NULL WHERE task_id=?').run(state,spec.period_seconds?this.now()+spec.period_seconds*1000:this.now(),a.session_id,json({outcome:a.outcome,summary:a.summary,at:this.now(),late:!valid,spec_revision:r.revision}),a.error??null,r.task_id);
|
|
169
|
+
this.slots.release(r.task_id,r.claim,{drained:true});if(valid&&a.outcome==='ready')this.schedules.complete(r);this.follow(r.task_id);
|
|
170
|
+
this.observed(r.task_id,Math.max(0,r.runs-1));
|
|
171
|
+
const result={recorded:true,state:this.get(r.task_id).state,task_id:r.task_id};this.db.prepare('INSERT INTO managed_execution_receipts VALUES (?,?,?)').run(a.claim,payload,json(result));return result;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Only never-started expired claims are reclaimable. Running/unknown work keeps
|
|
175
|
+
// its slot until its owning endpoint reports actual drain, even after reboot.
|
|
176
|
+
reconcile(){this.schedules.tick();for(const r of this.db.prepare("SELECT * FROM managed_executions WHERE state='claimed' AND lease_until<=?").all(this.now()))atomic(this.db,()=>{this.db.prepare('INSERT INTO managed_unstarted_reclaims VALUES (?,?,?,?)').run(r.claim,r.task_id,r.instance,r.role);this.slots.release(r.task_id,r.claim,{drained:true});this.db.prepare("UPDATE managed_executions SET state='queued',claim=NULL,instance=NULL WHERE task_id=?").run(r.task_id);});
|
|
177
|
+
for(const r of this.db.prepare("SELECT task_id FROM managed_executions WHERE state IN ('idle','waiting','waiting_input','queued')").all())this.follow(r.task_id);
|
|
178
|
+
}
|
|
179
|
+
assertResume(id){const r=this.get(id);if(!r)return;if(r.claim||this.busy(id))fail('NOT_QUIESCENT');this.authority(r,{resuming:true});if(!r.parked_state||!['queued','waiting','waiting_input','idle','scheduled'].includes(r.parked_state))fail('RESUME_NOT_READY');}
|
|
180
|
+
control(id,action){
|
|
181
|
+
const r=this.get(id);if(!r)return;this.observed(id,r.runs);this.schedules.control(id,action);
|
|
182
|
+
if(action==='pause'){const periodic=JSON.parse(r.spec).period_seconds;const parked=['claimed','running','queued'].includes(r.state)?(periodic?'waiting':'queued'):r.state;this.db.prepare('UPDATE managed_executions SET parked_state=? WHERE task_id=?').run(parked,id);}
|
|
183
|
+
if(action==='stop')this.db.prepare("UPDATE managed_executions SET parked_state=NULL,state=CASE WHEN claim IS NULL THEN 'fenced' ELSE state END WHERE task_id=?").run(id);
|
|
184
|
+
if(action==='resume'&&r.parked_state){
|
|
185
|
+
const period=JSON.parse(r.spec).period_seconds;this.authority(r);const valid=true;
|
|
186
|
+
this.db.prepare('UPDATE managed_executions SET state=?,due=?,parked_state=NULL WHERE task_id=?').run(valid?r.parked_state:'fenced',r.parked_state==='waiting'&&period?this.now()+period*1000:this.now(),id);
|
|
187
|
+
if(valid)this.follow(id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
suspend(role,a){
|
|
191
|
+
const done=this.db.prepare('SELECT payload FROM managed_execution_receipts WHERE claim=?').get(a.claim);
|
|
192
|
+
if(done){const p=JSON.parse(done.payload);if(p.task_id!==a.task_id||p.instance_id!==a.instance_id||this.get(a.task_id)?.role!==role)fail('CLAIM_DENIED');return {historical_receipt:true};}
|
|
193
|
+
const r=this.owned(role,a);return atomic(this.db,()=>{
|
|
194
|
+
const state=this.g.continuity.state(r.task_id);if(state.state==='active'){this.control(r.task_id,'pause');this.db.prepare("UPDATE task_continuity SET state='paused',version=version+1,control_id=NULL,targets='[]' WHERE task_id=?").run(r.task_id);this.db.prepare("UPDATE managed_executions SET error='TASK_EXECUTION_NATIVE_ARCHIVE_STOP' WHERE task_id=?").run(r.task_id);}
|
|
195
|
+
return {paused:this.g.continuity.state(r.task_id).state==='paused',task_id:r.task_id};
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
assertCompletion(r,c){
|
|
199
|
+
const latest=this.db.prepare("SELECT max(seq) n FROM task_inputs WHERE task_id=? AND mode='follow' AND state<>'handed_back'").get(r.task_id).n??0;
|
|
200
|
+
if(!c||c.round!==r.runs||c.input_cursor!==r.input_cursor||latest>r.input_cursor||c.control_version!==this.g.continuity.state(r.task_id).version)fail('COMPLETION_STALE');
|
|
201
|
+
}
|
|
202
|
+
recover(role,a){const r=this.owned(role,a);this.authority(r);if(r.state!=='running')fail('NOT_RUNNING');this.db.prepare('UPDATE managed_executions SET lease_until=? WHERE task_id=?').run(this.now()+30000,r.task_id);return this.item(this.get(r.task_id));}
|
|
203
|
+
grantId(r){return r.authority?'task:'+r.task_id+':'+r.revision+':'+r.runs+':'+r.input_cursor:r.approval_id;}
|
|
204
|
+
async deliver(role,a){this.validate(role,a);const r=this.owned(role,a),d=JSON.parse(r.spec).development,n=d?.notification;if(!n)fail('DELIVERY_AGREEMENT_REQUIRED');if(r.authority)this.assertCompletion(r,a.completion);const artifact=this.g.artifacts.load(r.task_id,a.artifact_id);if(artifact.kind!=='native_file'||artifact.spec_revision!==r.revision||!d.artifacts.includes(artifact.path)||a.current_sha256!==artifact.sha256)fail('ARTIFACT_NOT_AUTHORIZED');return this.g.notifications.media.enqueueTask(role,{task_id:r.task_id,artifact_id:a.artifact_id,mode:'file',expected_revision:r.revision,expected_binding_revision:n.binding_revision,event_id:'managed-delivery:'+this.grantId(r)+':'+artifact.id},this.grantId(r),n.target_key);}
|
|
205
|
+
artifact(role,a){this.validate(role,a);const r=this.owned(role,a),d=JSON.parse(r.spec).development;if(!d||!d.artifacts.includes(a.path))fail('ARTIFACT_NOT_AUTHORIZED');return this.g.artifacts.nativeChunk(r,a);}
|
|
206
|
+
call(method,a,role){if(!['local','cloud'].includes(role))fail('ROLE_INVALID');const m=method.slice('execution_'.length);if(m==='enqueue')return this.enqueue(a);if(m==='status')return this.view(a.task_id);if(!['usage','recover','claim','begin','validate','reserve','inputs','finish','suspend','evidence','artifact','deliver'].includes(m))fail('METHOD_DENIED');return this[m](role,a);}
|
|
207
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Task policy is not an OS sandbox. These guards catch explicit high-risk
|
|
2
|
+
// requests; arbitrary shell programs still run with the cloud account's rights.
|
|
3
|
+
import {homedir} from 'node:os';
|
|
4
|
+
import {resolve,relative,isAbsolute,dirname,basename} from 'node:path';
|
|
5
|
+
import {realpathSync} from 'node:fs';
|
|
6
|
+
const fail=s=>{throw Error('TASK_EXECUTION_'+s);};
|
|
7
|
+
export const under=(root,path)=>{const rel=relative(root,path);return rel===''||rel!=='..'&&!rel.startsWith('../')&&!isAbsolute(rel);};
|
|
8
|
+
const physical=p=>{p=resolve(p);try{return realpathSync(p);}catch(e){if(e.code!=='ENOENT'&&e.code!=='ENOTDIR')throw e;const parent=dirname(p);return parent===p?p:resolve(physical(parent),basename(p));}};
|
|
9
|
+
export function assertExplicitRisk(exec,cwd,{stateDir,protectedRoots=[],development,role='cloud'}={}){
|
|
10
|
+
const roots=[stateDir,process.env.DSH_HOME,resolve(homedir(),'.ssh'),resolve(homedir(),'.aws'),...protectedRoots].filter(Boolean).map(physical);
|
|
11
|
+
const arg=exec.arguments??{},path=arg.file_path??arg.path;
|
|
12
|
+
if(typeof path==='string'&&roots.some(root=>under(root,physical(resolve(cwd,path)))))fail('PROTECTED_DATA');
|
|
13
|
+
if(exec.name==='bash'){
|
|
14
|
+
const command=arg.command;if(typeof command!=='string')fail('COMMAND_INVALID');
|
|
15
|
+
if(/\b(?:sudo|su|systemctl|launchctl|shutdown|reboot|mkfs(?:\.\w+)?|fdisk|parted)\b|\brm\s+[^\n;&|]*-[\w]*[rR]|\bgit\b[^\n;&|]*\b(?:reset|clean|rebase|merge)\b|\bgit\b[^\n;&|]*(?:--force|--mirror|--delete|--amend|\s-[a-zA-Z]*f(?:\s|$))|\bgit\s+(?:branch\s+-[dD]|tag\s+-d)|\b(?:npm|pnpm|yarn)\s+publish\b|\b(?:gh|glab)\s+(?:(?:pr|mr)\s+(?:merge|close)|release\s+create)\b|\b(?:kubectl|terraform)\b[^\n;&|]*\b(?:apply|destroy|delete)\b/.test(command))fail('HIGH_RISK_REVIEW_REQUIRED');
|
|
16
|
+
const mode=role==='cloud'?development?.delivery:null;
|
|
17
|
+
if(/\bgit\b[^\n;&|]*\bcommit\b/.test(command)&&!['commit','push','pr'].includes(mode)||/\bgit\b[^\n;&|]*\bpush\b/.test(command)&&!['push','pr'].includes(mode)||/\b(?:gh|glab)\s+(?:pr|mr)\s+create\b/.test(command)&&mode!=='pr')fail('HIGH_RISK_REVIEW_REQUIRED');
|
|
18
|
+
if(roots.some(p=>command.includes(p))||/(?:~\/|\$HOME\/)(?:\.ssh|\.aws|\.dsh)(?:[\/\s]|$)/.test(command))fail('PROTECTED_DATA');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// R3 primitives shared by the coordinator and managed execution endpoints.
|
|
2
|
+
// No IM identity, session deletion, implicit retry or replacement-session recovery.
|
|
3
|
+
import {CLOUD_TASK_POLICY} from './managed-cloud-policy.js';
|
|
4
|
+
import {randomUUID} from 'node:crypto';
|
|
5
|
+
import {isAbsolute,normalize} from 'node:path';
|
|
6
|
+
|
|
7
|
+
const json=JSON.stringify;
|
|
8
|
+
export const ARCHIVE_IDLE_MS=7*86400000;
|
|
9
|
+
const fail=code=>{throw Error('TASK_RUNTIME_'+code);};
|
|
10
|
+
const text=s=>typeof s==='string'&&s.length>0&&s.length<=512&&!s.includes('\0');
|
|
11
|
+
const atomic=(db,fn)=>{if(db.isTransaction)return fn();db.exec('BEGIN IMMEDIATE');try{const r=fn();db.exec('COMMIT');return r;}catch(e){if(db.isTransaction)db.exec('ROLLBACK');throw e;}};
|
|
12
|
+
|
|
13
|
+
// DSH rc.1 exposes Agent.model; rc.5 exposes a scoped public selection helper.
|
|
14
|
+
// Keep one selection per live Agent and release its listeners with that scope.
|
|
15
|
+
const legacySelections=new WeakMap();
|
|
16
|
+
export async function selectManagedModel(agent,policy,load=()=>import('@deepseek-ai/dsh-agent')){
|
|
17
|
+
if(typeof agent?.model?.select==='function'){agent.model.select(policy);return;}
|
|
18
|
+
const existing=legacySelections.get(agent);if(existing){existing.current={...policy};return;}
|
|
19
|
+
const native=await load();
|
|
20
|
+
if(typeof native.installModelSelection!=='function'||native.installModelSelection.length<2||typeof agent?.ctx?.effect!=='function')throw Error('TASK_EXECUTION_MODEL_SELECTION_UNAVAILABLE');
|
|
21
|
+
const selection={current:{...policy},assembled:undefined};
|
|
22
|
+
const dispose=native.installModelSelection(agent.ctx,selection);
|
|
23
|
+
agent.ctx.effect(()=>()=>{dispose();legacySelections.delete(agent);});
|
|
24
|
+
legacySelections.set(agent,selection);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Same meaning as prototype WorkerSlots: a starting or background-settling worker
|
|
28
|
+
// still holds its slot. Durable ownership also prevents an offline peer's lease
|
|
29
|
+
// expiry from granting an unsafe replacement. Only a verified drain releases it.
|
|
30
|
+
export class ManagedSlots {
|
|
31
|
+
constructor(db,now=Date.now,{localEnabled=true}={}){
|
|
32
|
+
this.db=db;this.now=now;this.localEnabled=localEnabled;
|
|
33
|
+
db.exec(`CREATE TABLE IF NOT EXISTS managed_slots(
|
|
34
|
+
task_id TEXT PRIMARY KEY,claim TEXT UNIQUE NOT NULL,role TEXT NOT NULL,
|
|
35
|
+
resources TEXT NOT NULL,person INTEGER NOT NULL,created INTEGER NOT NULL);`);
|
|
36
|
+
}
|
|
37
|
+
list(){return this.db.prepare('SELECT * FROM managed_slots ORDER BY created,task_id').all();}
|
|
38
|
+
reserve({task_id,role,resources=[],humanFollowup=false}){
|
|
39
|
+
if(!text(task_id)||!['local','cloud'].includes(role)||typeof humanFollowup!=='boolean'||!Array.isArray(resources)||resources.length>16||resources.some(x=>!text(x))||new Set(resources).size!==resources.length)fail('SLOT_INVALID');
|
|
40
|
+
if(role==='local'&&!this.localEnabled)return {reserved:false,reason:'local_execution_retired'};
|
|
41
|
+
return atomic(this.db,()=>{
|
|
42
|
+
const rows=this.list();
|
|
43
|
+
if(rows.some(r=>r.task_id===task_id))return {reserved:false,reason:'task_running'};
|
|
44
|
+
if(rows.some(r=>JSON.parse(r.resources).some(x=>resources.includes(x))))return {reserved:false,reason:'resource_busy'};
|
|
45
|
+
// CR-02: cloud 3+1, local one independently bounded worker, not a shared pool.
|
|
46
|
+
const used=rows.filter(r=>r.role===role).length,limit=role==='local'?1:3+(humanFollowup?1:0);
|
|
47
|
+
if(used>=limit)return {reserved:false,reason:'worker_busy'};
|
|
48
|
+
const claim=randomUUID();
|
|
49
|
+
this.db.prepare('INSERT INTO managed_slots VALUES (?,?,?,?,?,?)').run(task_id,claim,role,json([...resources].sort()),Number(humanFollowup),this.now());
|
|
50
|
+
return {reserved:true,claim,task_id,extra_slot:role==='cloud'&&used>=3};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
assert(task,claim){const r=this.db.prepare('SELECT * FROM managed_slots WHERE task_id=?').get(task);if(!r||r.claim!==claim)fail('STALE_SLOT');return r;}
|
|
54
|
+
release(task,claim,{drained}={}){this.assert(task,claim);if(drained!==true)fail('DRAIN_REQUIRED');this.db.prepare('DELETE FROM managed_slots WHERE task_id=? AND claim=?').run(task,claim);}
|
|
55
|
+
snapshot(){const rows=this.list();return {ordinary:3,human_followup_extra:1,local_limit:this.localEnabled?1:0,cloud_held:rows.filter(r=>r.role==='cloud').length,local_held:rows.filter(r=>r.role==='local').length,held:rows.length,unknown_owners_still_hold:true};}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Counts actual dispatched model requests, not model turns or token billing.
|
|
59
|
+
// The caller must route root, descendant, auxiliary and retry streams here BEFORE
|
|
60
|
+
// dispatch. Failed/unknown calls are charged; a restart never refunds them.
|
|
61
|
+
export class ManagedBudget {
|
|
62
|
+
constructor(db,now=Date.now){
|
|
63
|
+
this.db=db;this.now=now;
|
|
64
|
+
db.exec(`CREATE TABLE IF NOT EXISTS managed_budgets(task_id TEXT PRIMARY KEY,policy TEXT NOT NULL,max_requests INTEGER NOT NULL,end_at INTEGER NOT NULL);
|
|
65
|
+
CREATE TABLE IF NOT EXISTS managed_model_requests(id TEXT PRIMARY KEY,task_id TEXT NOT NULL,purpose TEXT NOT NULL,created INTEGER NOT NULL);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS managed_model_requests_task ON managed_model_requests(task_id);`);
|
|
67
|
+
if(!db.prepare('PRAGMA table_info(managed_model_requests)').all().some(x=>x.name==='usage'))db.exec('ALTER TABLE managed_model_requests ADD COLUMN usage TEXT');
|
|
68
|
+
if(!db.prepare('PRAGMA table_info(managed_model_requests)').all().some(x=>x.name==='policy'))db.exec('ALTER TABLE managed_model_requests ADD COLUMN policy TEXT');
|
|
69
|
+
if(!db.prepare('PRAGMA table_info(managed_budgets)').all().some(x=>x.name==='mode'))db.exec("ALTER TABLE managed_budgets ADD COLUMN mode TEXT NOT NULL DEFAULT 'capped'");
|
|
70
|
+
if(!db.prepare('PRAGMA table_info(managed_budgets)').all().some(x=>x.name==='search'))db.exec('ALTER TABLE managed_budgets ADD COLUMN search TEXT');
|
|
71
|
+
}
|
|
72
|
+
bind(task,policy,{maxRequests,endAt,search,executionPolicy},{amend=false}={}){
|
|
73
|
+
const unlimited=executionPolicy===CLOUD_TASK_POLICY;
|
|
74
|
+
if(unlimited){maxRequests=0;endAt??=0;} // explicit no-limit discriminator, never a huge cap
|
|
75
|
+
if(!text(task)||!policy||!text(policy.provider)||!text(policy.model)||Object.keys(policy).some(k=>!['provider','model','reasoningEffort'].includes(k))||policy.reasoningEffort!==undefined&&!text(policy.reasoningEffort)||(!unlimited&&(!Number.isSafeInteger(maxRequests)||maxRequests<1||maxRequests>100000))||!Number.isSafeInteger(endAt)||endAt!==0&&endAt<=this.now()||!unlimited&&endAt===0)fail('BUDGET_INVALID');
|
|
76
|
+
const saved={provider:policy.provider,model:policy.model,...(policy.reasoningEffort!==undefined?{reasoningEffort:policy.reasoningEffort}:{})};
|
|
77
|
+
const old=this.db.prepare('SELECT * FROM managed_budgets WHERE task_id=?').get(task);
|
|
78
|
+
const auxiliary=search?json(search):null;
|
|
79
|
+
const mode=unlimited?'unlimited':'capped';
|
|
80
|
+
if(old){
|
|
81
|
+
if(!unlimited&&amend&&search&&search.max_requests<this.searchUsed(task))fail('BUDGET_BELOW_USED');
|
|
82
|
+
if(amend){if(!unlimited&&maxRequests<this.view(task).used)fail('BUDGET_BELOW_USED');this.db.prepare('UPDATE managed_budgets SET policy=?,max_requests=?,end_at=?,search=?,mode=? WHERE task_id=?').run(json(saved),maxRequests,endAt,auxiliary,mode,task);}
|
|
83
|
+
else if(old.policy!==json(saved)||old.max_requests!==maxRequests||old.end_at!==endAt||old.search!==auxiliary||old.mode!==mode)fail('BUDGET_CHANGED');
|
|
84
|
+
return this.view(task);
|
|
85
|
+
}
|
|
86
|
+
this.db.prepare('INSERT INTO managed_budgets(task_id,policy,max_requests,end_at,search,mode) VALUES (?,?,?,?,?,?)').run(task,json(saved),maxRequests,endAt,auxiliary,mode);return this.view(task);
|
|
87
|
+
}
|
|
88
|
+
view(task){const r=this.db.prepare('SELECT * FROM managed_budgets WHERE task_id=?').get(task);if(!r)fail('BUDGET_NOT_FOUND');return {...r,max_requests:r.mode==='unlimited'?null:r.max_requests,end_at:r.end_at||null,policy:JSON.parse(r.policy),search:r.search?JSON.parse(r.search):null,search_used:this.searchUsed(task),usage_status:'unknown-unless-native-reported',request_count_scope:'native llm streams and managed_search; native web_search internal requests unknown',used:this.db.prepare('SELECT count(*) n FROM managed_model_requests WHERE task_id=?').get(task).n};}
|
|
89
|
+
usage(task,id,usage){
|
|
90
|
+
if(!id||!usage||typeof usage!=='object')return {recorded:false,status:'unknown'};
|
|
91
|
+
const safe=Object.fromEntries(Object.entries(usage).filter(([k,v])=>/^(inputTokens|outputTokens|totalTokens|cachedInputTokens)$/.test(k)&&Number.isFinite(v)&&v>=0));
|
|
92
|
+
if(!Object.keys(safe).length)return {recorded:false,status:'unknown'};
|
|
93
|
+
try{const r=this.db.prepare('UPDATE managed_model_requests SET usage=? WHERE task_id=? AND id=?').run(json(safe),task,id);return {recorded:r.changes===1,status:r.changes?'reported':'unknown'};}catch{return {recorded:false,status:'unknown'};}
|
|
94
|
+
}
|
|
95
|
+
searchUsed(task){return this.db.prepare("SELECT count(*) n FROM managed_model_requests WHERE task_id=? AND purpose='managed-search'").get(task).n;}
|
|
96
|
+
reserve(task,policy,purpose,{search=false}={}){
|
|
97
|
+
if(!text(purpose))fail('REQUEST_INVALID');
|
|
98
|
+
return atomic(this.db,()=>{
|
|
99
|
+
const b=this.view(task),p=search?b.search?.model_policy:b.policy;
|
|
100
|
+
if(search&&(!p||b.mode!=='unlimited'&&b.search_used>=b.search.max_requests))fail('SEARCH_BUDGET_EXHAUSTED');
|
|
101
|
+
if(!search&&purpose==='managed-search')fail('REQUEST_INVALID');
|
|
102
|
+
if(policy?.provider!==p.provider||policy?.model!==p.model||p.reasoningEffort!==undefined&&policy.reasoningEffort!==p.reasoningEffort)fail('MODEL_CHANGED');
|
|
103
|
+
if(b.end_at&&this.now()>=b.end_at)fail('DEADLINE');if(b.mode!=='unlimited'&&b.used>=b.max_requests)fail('BUDGET_EXHAUSTED');
|
|
104
|
+
const id=randomUUID();this.db.prepare('INSERT INTO managed_model_requests(id,task_id,purpose,created,policy) VALUES (?,?,?,?,?)').run(id,task,search?'managed-search':purpose,this.now(),json(p));return {id,used:b.used+1,limit:b.max_requests};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Persist exactly the target Host's resolved route. An execution-role default is
|
|
110
|
+
// subordinate to an explicit task route; absent effort is not forced to `high`.
|
|
111
|
+
export async function resolveManagedModel(ctx,explicit,roleDefault){
|
|
112
|
+
const input=explicit??roleDefault??{};
|
|
113
|
+
if(!ctx.llm?.resolveCallConfig)fail('MODEL_UNAVAILABLE');
|
|
114
|
+
const p=await ctx.llm.resolveCallConfig(input,AbortSignal.timeout(5000));
|
|
115
|
+
if(!text(p?.provider)||!text(p?.model)||input.provider&&p.provider!==input.provider||input.model&&p.model!==input.model||input.reasoningEffort!==undefined&&p.reasoningEffort!==input.reasoningEffort)fail('MODEL_CHANGED');
|
|
116
|
+
return {provider:p.provider,model:p.model,...(p.reasoningEffort!==undefined?{reasoningEffort:p.reasoningEffort}:{})};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class ManagedSessions {
|
|
120
|
+
constructor(ctx,db,{now=Date.now,canResume=()=>false,canArchive=()=>false,hasBackground=async()=>true}={}){
|
|
121
|
+
this.ctx=ctx;this.db=db;this.now=now;this.canResume=canResume;this.canArchive=canArchive;this.hasBackground=hasBackground;this.handles=new Map();this.pending=new Set();
|
|
122
|
+
db.exec(`CREATE TABLE IF NOT EXISTS managed_sessions(task_id TEXT PRIMARY KEY,role TEXT NOT NULL,
|
|
123
|
+
session_id TEXT UNIQUE NOT NULL,workspace TEXT NOT NULL,state TEXT NOT NULL,last_used INTEGER NOT NULL,error TEXT);`);
|
|
124
|
+
}
|
|
125
|
+
get(task){return this.db.prepare('SELECT * FROM managed_sessions WHERE task_id=?').get(task);}
|
|
126
|
+
owned(id){return this.db.prepare('SELECT * FROM managed_sessions WHERE session_id=?').get(id);}
|
|
127
|
+
touch(id){if(this.owned(id))this.db.prepare('UPDATE managed_sessions SET last_used=? WHERE session_id=?').run(this.now(),id);}
|
|
128
|
+
update(task,state,error=null){this.db.prepare('UPDATE managed_sessions SET state=?,error=? WHERE task_id=?').run(state,error,task);}
|
|
129
|
+
async open({task_id,role,workspace,agentOptions,setup}){
|
|
130
|
+
if(!text(task_id)||!['local','cloud'].includes(role)||!text(workspace)||!isAbsolute(workspace)||normalize(workspace)!==workspace)fail('SESSION_INVALID');
|
|
131
|
+
if(this.pending.has(task_id)||this.handles.has(task_id))fail('SESSION_BUSY');
|
|
132
|
+
if(!await this.canResume(task_id))fail('EXECUTION_FENCED');
|
|
133
|
+
this.pending.add(task_id);let handle,changed=false;
|
|
134
|
+
try{
|
|
135
|
+
let row=this.get(task_id),fresh=!row;
|
|
136
|
+
if(row&&(row.role!==role||row.workspace!==workspace))fail('SESSION_BINDING_CHANGED');
|
|
137
|
+
if(row&&['creating','running','uncertain'].includes(row.state))fail('SESSION_RECONCILE_REQUIRED');
|
|
138
|
+
if(!this.ctx.workspaceRegistry?.unarchiveSession||!this.ctx.workspaceRegistry?.archiveSession)fail('ARCHIVE_API_UNAVAILABLE');
|
|
139
|
+
if(!row){
|
|
140
|
+
const id='managed-'+randomUUID();
|
|
141
|
+
this.db.prepare("INSERT INTO managed_sessions VALUES (?,?,?,?,'creating',?,NULL)").run(task_id,role,id,workspace,this.now());row=this.get(task_id);changed=true;
|
|
142
|
+
}
|
|
143
|
+
// Public API deliberately does not prove existence. resume() MUST load the
|
|
144
|
+
// same persisted identity successfully; never fall back to create().
|
|
145
|
+
if(!fresh){changed=true;await this.ctx.workspaceRegistry.unarchiveSession(row.session_id);}
|
|
146
|
+
if(!await this.canResume(task_id))fail('EXECUTION_FENCED');
|
|
147
|
+
const options={agentOptions,setup:async(a,agent)=>{
|
|
148
|
+
if(agent.id!==row.session_id||agent.session.header.cwd!==workspace)fail('SESSION_BINDING_CHANGED');
|
|
149
|
+
const inner=await setup?.(a,agent);
|
|
150
|
+
// The publication gate is synchronous by native contract. Do not mistake
|
|
151
|
+
// an unresolved Promise for an affirmative authority decision.
|
|
152
|
+
return {commit:()=>{if(this.canResume(task_id)!==true)fail('EXECUTION_FENCED');inner?.commit?.();}};
|
|
153
|
+
}};
|
|
154
|
+
handle=await this.ctx.agents.withoutInitiator(()=>fresh?
|
|
155
|
+
this.ctx.agents.create({...options,sessionId:row.session_id,meta:{cwd:workspace}}):
|
|
156
|
+
this.ctx.agents.resume({...options,resumeSessionId:row.session_id}));
|
|
157
|
+
if(!await this.canResume(task_id))fail('EXECUTION_FENCED');
|
|
158
|
+
this.handles.set(task_id,handle);this.update(task_id,'running');this.touch(row.session_id);return handle;
|
|
159
|
+
}catch(e){
|
|
160
|
+
if(handle)try{await handle.dispose();}catch{this.handles.set(task_id,handle);}
|
|
161
|
+
if(changed)this.update(task_id,'uncertain',String(e.code??e.message).slice(0,200));throw e;
|
|
162
|
+
}finally{this.pending.delete(task_id);}
|
|
163
|
+
}
|
|
164
|
+
async idle(task){
|
|
165
|
+
const handle=this.handles.get(task),row=this.get(task);if(!handle||!row)fail('SESSION_NOT_OWNED');
|
|
166
|
+
try{
|
|
167
|
+
if(handle.agent.status!=='idle'||await this.hasBackground(row.session_id))fail('SESSION_BUSY');
|
|
168
|
+
if(await this.ctx.sessions.flush(handle.agent.session)!==true)fail('PERSISTENCE_UNAVAILABLE');
|
|
169
|
+
await handle.dispose();this.handles.delete(task);this.update(task,'idle');this.touch(row.session_id);
|
|
170
|
+
}catch(e){this.update(task,'uncertain',String(e.code??e.message).slice(0,200));throw e;}
|
|
171
|
+
}
|
|
172
|
+
async archiveIdle(){
|
|
173
|
+
const results=[];
|
|
174
|
+
for(const row of this.db.prepare("SELECT * FROM managed_sessions WHERE state='idle' AND last_used<=?").all(this.now()-ARCHIVE_IDLE_MS)){
|
|
175
|
+
if(this.pending.has(row.task_id)||this.handles.has(row.task_id)||!await this.canArchive(row.task_id)||await this.hasBackground(row.session_id)||this.ctx.agents.get(row.session_id))continue;
|
|
176
|
+
this.pending.add(row.task_id);
|
|
177
|
+
try{
|
|
178
|
+
// No stopActivity: the native registry independently refuses active work.
|
|
179
|
+
await this.ctx.workspaceRegistry.archiveSession(row.session_id);
|
|
180
|
+
this.update(row.task_id,'archived');results.push({task_id:row.task_id,archived:true});
|
|
181
|
+
}catch(e){results.push({task_id:row.task_id,archived:false,error:String(e.code??e.message).slice(0,200)});}
|
|
182
|
+
finally{this.pending.delete(row.task_id);}
|
|
183
|
+
}
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {validateAutonomousSearch} from './search-contract.js';
|
|
2
|
+
// Reuse the existing public DeepSeek search adapter; no second search provider.
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
5
|
+
import {createTaskSearchReader} from './source-search.js';
|
|
6
|
+
import {sourceUrl} from './source-web.js';
|
|
7
|
+
const fail=s=>{throw Error('TASK_SEARCH_'+s);};
|
|
8
|
+
export function validateManagedSearch(c){if(c!==undefined&&(!c||typeof c!=='object'||Array.isArray(c)||Object.keys(c).some(k=>!['credentialRef','model_policy'].includes(k))||typeof c.credentialRef!=='string'||!/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(c.credentialRef)))fail('CONFIG_INVALID');if(c?.model_policy)validateAutonomousSearch({scope:'configured',model_policy:c.model_policy});}
|
|
9
|
+
export class ManagedSearch {
|
|
10
|
+
constructor(endpoint,config,createReader=createTaskSearchReader){
|
|
11
|
+
validateManagedSearch(config);this.ep=endpoint;this.db=endpoint.db;this.pending=new Map();
|
|
12
|
+
this.db.exec('CREATE TABLE IF NOT EXISTS managed_search_operations(id TEXT PRIMARY KEY,task_id TEXT NOT NULL,fingerprint TEXT NOT NULL,state TEXT NOT NULL,result TEXT,error TEXT);');
|
|
13
|
+
this.ready=config?createReader(endpoint.ctx,config).then(r=>{this.reader=r;}).catch(()=>{}):Promise.resolve();
|
|
14
|
+
}
|
|
15
|
+
async run(b,agent,a,callId,signal){
|
|
16
|
+
if(!a||Object.keys(a).join()!=='query'||typeof a.query!=='string'||!a.query.trim()||a.query.length>300||/[\x00-\x1f]/.test(a.query)||typeof callId!=='string'||!callId||callId.length>256)fail('ARGUMENT_INVALID');
|
|
17
|
+
const policy=b.item.spec.search;if(!policy)fail('NOT_AUTHORIZED');
|
|
18
|
+
await this.ready;if(!this.reader)fail('UNAVAILABLE');
|
|
19
|
+
await this.ep.validate(b);
|
|
20
|
+
const id=JSON.stringify([b.item.claim,agent.id,callId]),fp=createHash('sha256').update(JSON.stringify([b.item.task_id,policy,a.query])).digest('hex');
|
|
21
|
+
const old=this.db.prepare('SELECT * FROM managed_search_operations WHERE id=?').get(id);
|
|
22
|
+
if(old){if(old.fingerprint!==fp)fail('EVENT_CONFLICT');if(old.state==='completed')return JSON.parse(old.result);if(this.pending.has(id))return this.pending.get(id);fail(old.error??'DISPATCH_UNCERTAIN');}
|
|
23
|
+
if(this.db.prepare("SELECT 1 FROM managed_search_operations WHERE task_id=? AND state IN ('pending','uncertain')").get(b.item.task_id))fail('DISPATCH_UNCERTAIN');
|
|
24
|
+
this.db.prepare("INSERT INTO managed_search_operations VALUES (?,?,?,'pending',NULL,NULL)").run(id,b.item.task_id,fp);
|
|
25
|
+
const work=this.dispatch(b,a,policy,id,signal).finally(()=>this.pending.delete(id));this.pending.set(id,work);return work;
|
|
26
|
+
}
|
|
27
|
+
async dispatch(b,a,policy,id,signal){
|
|
28
|
+
const deadline=AbortSignal.any([b.controller.signal,signal,AbortSignal.timeout(45000)].filter(Boolean));let reserved=false,recorded=false,returned=false;
|
|
29
|
+
try{
|
|
30
|
+
deadline.throwIfAborted();await this.ep.validate(b);
|
|
31
|
+
// Reserve before credential resolution/HTTP. A lost reserve acknowledgement is
|
|
32
|
+
// uncertain, never refunded/retried; native provider's sync callback cannot await A2A.
|
|
33
|
+
reserved=true;await this.ep.call('reserve',{task_id:b.item.task_id,claim:b.item.claim,instance_id:b.item.instance,channel:'search',policy:policy.model_policy,purpose:'managed-search'});
|
|
34
|
+
const expected={endpoint:'https://api.deepseek.com/anthropic/v1/messages',apiVersion:'2023-06-01',body:{model:policy.model_policy.model,max_tokens:2048,messages:[{role:'user',content:[{type:'text',text:'Perform a web search for the query: '+a.query}]}],tools:[{type:'web_search_20250305',name:'web_search',max_uses:1}],thinking:{type:'disabled'}}};
|
|
35
|
+
const result=await this.reader.search(a.query,policy.model_policy,deadline,request=>{
|
|
36
|
+
deadline.throwIfAborted();if(this.ep.closed||performance.now()>=b.validUntil)fail('CANCELLED');
|
|
37
|
+
if(recorded||!isDeepStrictEqual(request,expected))fail('REQUEST_CHANGED');recorded=true;
|
|
38
|
+
});returned=true;deadline.throwIfAborted();await this.ep.validate(b);
|
|
39
|
+
if(!recorded)fail('DISPATCH_UNVERIFIED');
|
|
40
|
+
if(!result||!Array.isArray(result.sources)||result.sources.length>100||typeof result.truncated!=='boolean')fail('RESULT_INVALID');
|
|
41
|
+
const sources=[],seen=new Set();let rejected=0;
|
|
42
|
+
for(const item of result.sources){let url;try{url=sourceUrl(item?.url).href;}catch{rejected++;continue;}
|
|
43
|
+
if(seen.has(url))continue;seen.add(url);
|
|
44
|
+
for(const [k,max] of [['title',500],['snippet',8000],['publishedAt',128]])if(item[k]!==undefined&&(typeof item[k]!=='string'||item[k].length>max||item[k].includes('\0')))fail('RESULT_INVALID');
|
|
45
|
+
if(sources.length<5)sources.push({url,...Object.fromEntries(['title','snippet','publishedAt'].filter(k=>item[k]!==undefined).map(k=>[k,item[k]]))});
|
|
46
|
+
}
|
|
47
|
+
const output={query:a.query,scope:policy.scope,model_policy:policy.model_policy,sources,truncated:result.truncated||seen.size>5,rejected_urls:rejected,page_content_read:false,verification:"search-snippets-only; read source body with native web_fetch before confirming claims; errors or truncated/missing body remain unverified",untrusted_external_content:true};
|
|
48
|
+
this.db.prepare("UPDATE managed_search_operations SET state='completed',result=? WHERE id=?").run(JSON.stringify(output),id);return output;
|
|
49
|
+
}catch(e){
|
|
50
|
+
const code=/^(TASK_SEARCH|TASK_RUNTIME|TASK_EXECUTION)_[A-Z_]+$/.test(e?.message)?e.message:'TASK_SEARCH_UNAVAILABLE';
|
|
51
|
+
// Reserve refusal is definitive. Network/ACK uncertainty is not a retry license.
|
|
52
|
+
const refused=/^TASK_RUNTIME_(?:SEARCH_BUDGET_EXHAUSTED|BUDGET_EXHAUSTED|MODEL_CHANGED|DEADLINE)$/.test(code);
|
|
53
|
+
const uncertain=reserved&&!returned&&!refused;
|
|
54
|
+
this.db.prepare('UPDATE managed_search_operations SET state=?,error=? WHERE id=?').run(uncertain?'uncertain':'failed',uncertain?'DISPATCH_UNCERTAIN':code.replace(/^TASK_SEARCH_/,''),id);throw Error(uncertain?'TASK_SEARCH_DISPATCH_UNCERTAIN':code);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {managedScheduleSchema} from './managed-triggers.js';
|
|
2
|
+
import {nativeModelChoice} from './native-model-choice.js';
|
|
3
|
+
import {developmentSchema} from './managed-development-contract.js';
|
|
4
|
+
import {currentHumanRequest} from './task-envelope.js';
|
|
5
|
+
import {hash} from './store.js';
|
|
6
|
+
export function registerManagedTools(g,register){
|
|
7
|
+
const str={type:'string'},policy={type:'object',properties:{provider:str,model:str,reasoningEffort:str},required:['provider','model'],additionalProperties:false};
|
|
8
|
+
const properties={schedule:managedScheduleSchema,task_id:str,expected_revision:{type:'integer',minimum:1},role:{type:'string',enum:['cloud']},cwd:str,development:{anyOf:[developmentSchema,{type:"null"}],description:"Required output agreement for ALL file tasks (reports, JSONL, data and code): use workspace delivery with real checks and relative artifact paths. Explicit null only for genuinely no-file tasks; omission is rejected."},scope:{type:'string',maxLength:3000},model_query:{type:'string',description:'User-written model name only; omit for saved task choice or cloud default.'},provider_hint:str,search:{type:'object',properties:{scope:{type:'string',maxLength:1000}},required:['scope'],additionalProperties:false},period_seconds:{type:'integer',minimum:60,maximum:31622400},end_at:{type:'integer',minimum:1}};
|
|
9
|
+
const start=async(a,e)=>{
|
|
10
|
+
const model_policy=await nativeModelChoice(g,a,e);
|
|
11
|
+
if(!Object.hasOwn(a,'development')||a.development===undefined)throw Error('TASK_OUTPUT_CONTRACT_REQUIRED: file tasks need development.artifacts/checks; use explicit null only when no file deliverable exists');
|
|
12
|
+
const {development,...rest}=a;
|
|
13
|
+
return g.call('execution_start',{...rest,...(development===null?{}:{development}),model_policy,role:'cloud',event_id:'native-execution:'+hash(g.c.role+':'+e.agent.id+':'+currentHumanRequest(e.agent).message_id+':'+e.callId)});
|
|
14
|
+
};
|
|
15
|
+
register('task_execute','Start/queue the saved explicit user task on CLOUD. No approval ID, IM permission, model request cap, fixed activity timeout or permission TTL. Omit model_query for the configured CLOUD default or saved explicit task choice; never invent a model name or resolve the entry model. Only quote a model name actually specified by the user. Search scope enables the already configured auxiliary route, without per-task model consent or a request sublimit. cwd is a working start, not a file sandbox. Native tools follow cloud account capabilities and user goal/restrictions. Optional end_at is ONLY an explicit business deadline, never an invented default. development is REQUIRED for every task: ALL file deliverables (including reports, research and JSONL) must list artifacts plus actual checks, workspace needs no Git; explicit null only for no-file tasks. Missing output agreement is an error, not a successfully queued file task; no per-artifact registration needed. Use schedule for explicit once/interval/weekly/dependency/named-signal plans; do not invent a deadline or business round count. schedule cannot combine with period_seconds. Followup does not fire a named signal. Periodic tasks reuse session, skip missed rounds, never silently switch model. Pause/stop via task_lifecycle. Historical capped/local contracts are not migrated or restarted.',properties,['task_id','expected_revision','cwd','development'],start);
|
|
16
|
+
register('task_execution_prepare','Compatibility name for task_execute on NEW cloud-autonomous tasks: this now queues execution immediately, not an approval proposal. Prefer task_execute. Existing historical approval records remain read-only and are never upgraded implicitly.',properties,['task_id','expected_revision','cwd','development'],start);
|
|
17
|
+
}
|