@volter/supercode-orchestrator 0.5.208 → 0.5.209
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/board/published.mjs +5 -1
- package/board/ztrack.mjs +8 -4
- package/connection.mjs +16 -7
- package/package.json +3 -4
- package/source-lock.mjs +7 -2
package/board/published.mjs
CHANGED
|
@@ -125,7 +125,11 @@ function agentFacts(db,taskId){
|
|
|
125
125
|
const agents=new Map(parse(checkpoint?.facts_json,[]).map(agent=>[JSON.stringify([agent.org,agent.assignment,agent.id]),agent]));
|
|
126
126
|
let scope=db.prepare('SELECT from_org,org,changed_at FROM supercode_card_agent_scopes WHERE task_id=?').get(taskId)??null;
|
|
127
127
|
let receipt=checkpoint?.receipt_sequence??0,declared=checkpoint?.declared_event_id??0;
|
|
128
|
-
|
|
128
|
+
// Match idx_events_task_kind's order. ORDER BY id alone lets SQLite choose idx_events_task_id and fetch every
|
|
129
|
+
// event's row just to reject its kind (including cards with no agent events). Those synchronous page reads held
|
|
130
|
+
// the owner's loop inside synchronizeAgents and expired unrelated manager lookups. Sort only the matching events.
|
|
131
|
+
const events=db.prepare(`SELECT id,kind,payload FROM task_events WHERE task_id=? AND kind IN ${AGENT_KINDS} ORDER BY kind,id`).all(taskId);
|
|
132
|
+
for(const event of events.sort((a,b)=>a.id-b.id)){
|
|
129
133
|
if(event.kind==='agent_declared')declared=Math.max(declared,event.id);
|
|
130
134
|
const value=parse(event.payload,{});
|
|
131
135
|
if(Number.isSafeInteger(value.receipt))receipt=Math.max(receipt,value.receipt);
|
package/board/ztrack.mjs
CHANGED
|
@@ -830,7 +830,6 @@ class ZtrackBoard {
|
|
|
830
830
|
if (this.get('rebuild_from_documents') || !this.one('SELECT 1 FROM main.tasks LIMIT 1')) return;
|
|
831
831
|
let fields = [];
|
|
832
832
|
try { fields = workflowFor(this.root).params.document_control_fields ?? []; } catch { /* no home policy */ }
|
|
833
|
-
if (!fields.length) return;
|
|
834
833
|
const value = (task, state, field) => field === 'status' ? state?.stage ?? task?.status ?? 'todo'
|
|
835
834
|
: field === 'blocked_by' ? json(state?.blocked_by, []).map((b) => [b.kind, b.id]).sort()
|
|
836
835
|
: field === 'paused' ? json(state?.paused, null)
|
|
@@ -862,15 +861,20 @@ class ZtrackBoard {
|
|
|
862
861
|
};
|
|
863
862
|
for (const [id, rows] of plan.rows) {
|
|
864
863
|
const task = this.one('SELECT * FROM main.tasks WHERE id = ?', id), state = this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', id);
|
|
864
|
+
// Stale status recovery is independent of a home's document-control policy. In particular, render() re-reads a
|
|
865
|
+
// refused span after a verb completes its card: its unchanged running line must not undo that completion.
|
|
866
|
+
// Reopening an ended card requires its workflow verb, even in a home that otherwise permits document edits.
|
|
867
|
+
const controlled = new Set(fields);
|
|
868
|
+
if (task && ENDED.has(value(task, state, 'status'))) controlled.add('status');
|
|
865
869
|
// A new display author is not an authenticated creator.
|
|
866
|
-
const differ =
|
|
870
|
+
const differ = [...new Set(['status', ...controlled])].filter((field) => (task || field !== 'created_by') && !same(value(rows.task, rows.state, field), value(task, state, field)));
|
|
867
871
|
const prior = task && differ.length ? rowsBefore(rows.state.subtask_of ?? id).get(id) : null;
|
|
868
872
|
const stale = prior ? differ.filter((field) => same(value(prior.task, prior.state, field), value(rows.task, rows.state, field))) : [];
|
|
869
873
|
for (const field of stale) adopt(rows, task, state, field);
|
|
870
|
-
const forbidden = differ.filter((field) => !stale.includes(field));
|
|
874
|
+
const forbidden = differ.filter((field) => controlled.has(field) && !stale.includes(field));
|
|
871
875
|
if (forbidden.length) plan.errors.set(id, `Document edit cannot change ${forbidden.join(', ')}; use this card's role-checked workflow verbs or request-state.`);
|
|
872
876
|
}
|
|
873
|
-
for (const id of plan.remove) {
|
|
877
|
+
for (const id of fields.length ? plan.remove : []) {
|
|
874
878
|
// A task line already absent from the replaced span was not removed by this edit: the store keeps it.
|
|
875
879
|
const parent = this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of;
|
|
876
880
|
const prior = parent && !plan.remove.has(parent) ? rowsBefore(parent) : null;
|
package/connection.mjs
CHANGED
|
@@ -101,14 +101,22 @@ export async function connectWorkplace(flags) {
|
|
|
101
101
|
if(Boolean(flags['webhook-url'])!==Boolean(flags['webhook-listen']))throw new Error('--webhook-url and --webhook-listen go together: where the Workplace sends the agents registration\'s events, and the loopback address the service listens on');
|
|
102
102
|
const moduleRoot=flags['modules-root']?resolve(flags['modules-root']):null;
|
|
103
103
|
// An explicit local credential input is useful for an owned stack; a normal connect uses its existing Teams context.
|
|
104
|
-
let teams,owner;
|
|
104
|
+
let teams,owner,teamsContext=flags.context??process.env.SUPERCODE_CONTEXT;
|
|
105
105
|
if(flags['teams-token-file']){
|
|
106
106
|
if(!flags['teams-url']||!flags.team)throw new Error('--teams-token-file needs --teams-url and --team');
|
|
107
107
|
teams={server:new URL(flags['teams-url']).origin,team:flags.team};owner=readFileSync(flags['teams-token-file'],'utf8').trim();
|
|
108
108
|
}else{
|
|
109
|
-
|
|
109
|
+
// An agent session connects as its agent: its delegated context, never the person's who signed the box in (the
|
|
110
|
+
// Teams verbs' rule, agentContextName). A person's context named from an agent session is refused, as a grant is.
|
|
110
111
|
const {WorkspaceContexts}=await import('@volter/teams/context');
|
|
111
|
-
|
|
112
|
+
const {agentSessionEnvironment,agentSessionHint,agentContextName}=await import('@volter/supercode-teams/agent-session');
|
|
113
|
+
const contexts=new WorkspaceContexts(supercodeHome());
|
|
114
|
+
// Asked of the daemon whatever the environment says: a resumed session can lack its launch's SUPERCODE_AGENT_ID,
|
|
115
|
+
// and the daemon's retained binding is what names its agent (workspace.mjs does the same for every Teams verb).
|
|
116
|
+
const env=await agentSessionEnvironment(process.env,agentSessionHint(process.env));
|
|
117
|
+
teamsContext=agentContextName(contexts,env,flags.context??null);
|
|
118
|
+
const {context}=fromContext({supercodeHome:supercodeHome(),name:teamsContext});
|
|
119
|
+
teams={server:context.origin,team:context.team_id};owner=contexts.credential(context.credential_ref).token;
|
|
112
120
|
}
|
|
113
121
|
for(const origin of [ticket.origin,ticket.issuer,teams.server]){
|
|
114
122
|
const url=new URL(origin);if(url.origin!==origin||url.username||url.password||(url.protocol!=='https:'&&!(url.protocol==='http:'&&['127.0.0.1','localhost','[::1]'].includes(url.hostname))))throw new Error('connection endpoints must be exact HTTPS origins or loopback HTTP');
|
|
@@ -116,7 +124,7 @@ export async function connectWorkplace(flags) {
|
|
|
116
124
|
const id=createHash('sha256').update(`${ticket.scope}\0${ticket.issuer}\0${ticket.organization}\0${teams.server}\0${teams.team}\0${flags.source}`).digest('hex').slice(0,24);
|
|
117
125
|
const path=connectionPath(root,id),prior=existsSync(path)?JSON.parse(readFileSync(path,'utf8')):null;
|
|
118
126
|
if(!prior&&!(Date.parse(ticket.expiresAt)>Date.now()))throw new Error('connection ticket expired; apply the organization file again');
|
|
119
|
-
const record=prior??{version:1,id,scope:ticket.scope,teamsContext
|
|
127
|
+
const record=prior??{version:1,id,scope:ticket.scope,teamsContext,origin:ticket.origin,issuer:ticket.issuer,organization:ticket.organization,teams,source:flags.source,
|
|
120
128
|
selection:ticket.grants.map(g=>g.name),moduleRoot,...(flags['webhook-url']?{webhook:{url:flags['webhook-url'],listen:flags['webhook-listen']}}:{}),floor:flags.floor??ticket.floor??'Work',alertRole:flags['alert-role']??'admin',grants:[]};
|
|
121
129
|
record.root=root;
|
|
122
130
|
if(prior?.disconnectedAt)throw new Error('this connection was disconnected; apply the organization file again for a new ticket');
|
|
@@ -187,7 +195,7 @@ export async function connectWorkplace(flags) {
|
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
/** Undo a connection with its own credentials, no admin needed (runhuman-2 ADR 0055 decision 3): its runner stops, every
|
|
190
|
-
* installation it holds leaves both sides by itself (each Workplace installation through its own
|
|
198
|
+
* installation it holds leaves both sides by itself (each Workplace installation through its own leave door, each
|
|
191
199
|
* Teams installation and the issuer's by deleting itself), and the home's registry entries go. The registrations stay, as
|
|
192
200
|
* consented apps with no installation; an admin deregisters them. The custody record stays, marked disconnected, for
|
|
193
201
|
* audit; it can never run again. */
|
|
@@ -211,10 +219,11 @@ export async function disconnectWorkplace(flags) {
|
|
|
211
219
|
};
|
|
212
220
|
for(const grant of record.grants){
|
|
213
221
|
if(grant.teams?.installation?.id)await leave(grant.name,'teams','DELETE',`${team}/app-installations/${encodeURIComponent(grant.teams.installation.id)}`,grant.teams.credential.token);
|
|
214
|
-
if(grant.rh2?.installationId)await leave(grant.name,'rh2','POST',`/api/v3/automation/installations/${encodeURIComponent(grant.rh2.installationId)}/
|
|
222
|
+
if(grant.rh2?.installationId)await leave(grant.name,'rh2','POST',`/api/v3/automation/installations/${encodeURIComponent(grant.rh2.installationId)}/leave`,grant.rh2.accessToken);
|
|
215
223
|
const entry=join(root,'apps',`${grant.name}.${record.id}.json`);if(existsSync(entry))unlinkSync(entry);
|
|
216
224
|
}
|
|
217
|
-
|
|
225
|
+
// The token exchange answers {installation, credential: {token, …}} (renewTeams reads the same shape).
|
|
226
|
+
if(record.issuerCredential?.installation?.id)await leave('issuer','teams','DELETE',`${team}/app-installations/${encodeURIComponent(record.issuerCredential.installation.id)}`,record.issuerCredential.credential?.token);
|
|
218
227
|
record.disconnectedAt=new Date().toISOString();record.left=left;privateJson(path,record);
|
|
219
228
|
return {connection:record.id,root,left,registrations:[...record.grants.map(g=>g.registration?.id).filter(Boolean),record.issuerRegistration?.id].filter(Boolean)};
|
|
220
229
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.209",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
|
|
6
6
|
"exports": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"check-types": "node scripts/generate-types.mjs --check"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@volter/supercode-teams": "^0.3.
|
|
35
|
+
"@volter/supercode-teams": "^0.3.344",
|
|
36
36
|
"@volter/supercode-terminal": "^0.3.10",
|
|
37
37
|
"@volter/supercode-harness-sdk": "^0.3.104",
|
|
38
38
|
"@volter/teams": "0.2.98",
|
|
@@ -61,6 +61,5 @@
|
|
|
61
61
|
"board",
|
|
62
62
|
"apply",
|
|
63
63
|
"README.md"
|
|
64
|
-
]
|
|
65
|
-
"gitHead": "db4a88b4d2f670654fa8e57dbc3e93c7daa8d2bb"
|
|
64
|
+
]
|
|
66
65
|
}
|
package/source-lock.mjs
CHANGED
|
@@ -10,6 +10,8 @@ if (lock) {
|
|
|
10
10
|
enforceSourceLock(import.meta.resolve('@volter/supercode-teams/source-lock'), 'the orchestrator Teams package');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
let driftSaid = false;
|
|
14
|
+
|
|
13
15
|
/** The locked tree's release native, proved against the same lock before any codec door opens. */
|
|
14
16
|
export function lockedNative() {
|
|
15
17
|
const lock = enforceSourceLock(import.meta.url, 'the orchestrator');
|
|
@@ -27,8 +29,11 @@ export function lockedNative() {
|
|
|
27
29
|
const result = spawnSync(binary, ['source', 'show', '--json'], { encoding: 'utf8', timeout: 5000 });
|
|
28
30
|
if (result.error || result.status !== 0) throw new SourceLockError(`supercode source lock: the orchestrator native ${binary} cannot be verified: ${result.error?.message || result.stderr}`);
|
|
29
31
|
const receipt = JSON.parse(result.stdout);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
// The native is judged by its binding alone: a retired copy that came back (drift) is said, and never stops the board.
|
|
33
|
+
const binding = receipt.binding ?? receipt.verdict;
|
|
34
|
+
if (binding !== 'matches' || receipt.native.version !== lock.supercode || receipt.native.commit !== lock.commit || receipt.native.dirty) {
|
|
35
|
+
throw new SourceLockError(`supercode source lock: the orchestrator native ${binary} does not match ${lock.path}: ${binding}`);
|
|
32
36
|
}
|
|
37
|
+
if (Array.isArray(receipt.drift) && receipt.drift.length && !driftSaid) { driftSaid = true; process.stderr.write(`supercode source lock: ${receipt.drift.join('; ')}\n`); }
|
|
33
38
|
return binary;
|
|
34
39
|
}
|