Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
066b11a
Add Project repository associations and non-destructive Owner closure
zy95-12 Nov 2, 2026
61e0abc
Separate personal-agent identities from Project participations
zy95-12 Nov 2, 2026
6eeba41
Authenticate scoped personal-agent connections with existing delegati…
zy95-12 Nov 2, 2026
b4fa8de
Preserve Task-role definitions and latest SDK session across retries
zy95-12 Nov 2, 2026
439a41c
Expose durable handoff contracts and private consultation flows
zy95-12 Nov 2, 2026
a9ff63b
Bound role context and retain scoped memory and working summaries
zy95-12 Nov 2, 2026
6ebc706
Cover scoped memory source pages and record real SDK continuation val…
zy95-12 Nov 2, 2026
4101275
Deliver fresh native Chief job context in the resident Session turn
zy95-12 Nov 2, 2026
70fbe82
Admit Project Tasks through Chief and resume parents after child results
zy95-12 Nov 2, 2026
4826723
Adjudicate dependency exceptions and admit verified child integration…
zy95-12 Nov 2, 2026
8f7dffa
Persist runtime state and Effects atomically in optional SQLite storage
zy95-12 Nov 2, 2026
90dfcc8
Unify durable command recovery and committed Project event interfaces
zy95-12 Nov 3, 2026
dba40f8
Require approved durable Project role changes
zy95-12 Nov 3, 2026
3e90554
Integrate approved role governance with personal agent identities
zy95-12 Nov 3, 2026
3629495
Integrate approved roles with current personal agent authorization
zy95-12 Nov 3, 2026
cbab78a
Merge branch 'feat/2183-agent-auth' into feat/2184-execution-lifecycle
zy95-12 Nov 3, 2026
a11696d
Require native execution quiescence before releasing Task context
zy95-12 Nov 3, 2026
f404153
Execute custom Project roles with pinned capability policies
zy95-12 Nov 3, 2026
32f7941
Activate exact native context before resuming pool handoffs
zy95-12 Nov 3, 2026
3f7fe22
Require consistent native exit proof across pool and supervisor recovery
zy95-12 Nov 3, 2026
29f0e02
Revalidate retained role execution and stop revoked claims through ex…
zy95-12 Nov 3, 2026
36210b9
Compose current execution and role governance with durable handoff co…
zy95-12 Nov 3, 2026
c16a25e
Compose bounded role context with current native execution and govern…
zy95-12 Nov 3, 2026
54a6bb5
Compose canonical Task waits with retained roles and fence changed re…
zy95-12 Nov 3, 2026
9d3ab4e
Compose transactional state and verify native continuation from commi…
zy95-12 Nov 3, 2026
cca28b2
Retain active-memory reference after preserved historical inventory
zy95-12 Nov 3, 2026
cbab790
Compose current execution into service queries, governance commands a…
zy95-12 Nov 3, 2026
dfde85c
Merge branch 'feat/2182-transactional-state' into feat/2185-service-c…
zy95-12 Nov 3, 2026
6e677c8
Keep role metadata reads pure and preserve complete policy within its…
zy95-12 Nov 3, 2026
86e8b6e
Initialize the custom artifact catalog through explicit owner configu…
zy95-12 Nov 3, 2026
a966241
Preserve original FlyBot capabilities across Project task cutover
zy95-12 Nov 3, 2026
c449cee
Sync delivery integration ancestry for #2285
zy95-12 Nov 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions LESSONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -767,3 +767,7 @@ Seen 1× (native projection regressions). A correct native stop/binding is insuf
### 2026-11-03 — Read-model lookups must not materialize domain facts

Seen 1× (actual SQLite API regression). A Task query requested full delivery definitions, creating an unsaved default role catalog. A later Task command persisted it and emitted an unrelated Project change. Return deterministic legacy defaults without mutation and use committed raw catalogs for metadata; keep materialization in explicit writes.

### 2026-11-03 — Absence of a new contract is not legacy admission

Seen 1× (group dispatch and Bot tracking reproduced the bypass). Capture existing Project identities once with committed evidence; later untyped rows must not inherit admission. Preserve original rework and explicit Workflow execution through trusted continuation/controller ports. Scope guards must run before mutating leases or claims, and physical storage rollback must retain the code-version fence.
4 changes: 4 additions & 0 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,7 @@ Composition checkpoint: current role governance/native exit, bounded context and
SQLite composition now has an actual DSH native return probe: canonical Chief intake, persisted activation/exit, original role context and single-resource handoff are read from committed SQL snapshots. Inference remains deterministic; no production migration or full-provider workflow acceptance is claimed.

Service composition adds scoped native execution/exit and current-vs-pinned role metadata, role governance aliases, and bounded per-entity committed snapshots. New projections and final six-case DSH/SQLite validation are under current review; none of this branch is deployed.

### 2026-11-03 — Preserve original capabilities (#2186)

Keep personal Chief/Bots, groups, private memories and original Task/Workflow IDs. Shared Project legacy execution requires an identity-bound committed cutover; new dispatch/tracking cannot bypass Chief admission. Associated original groups retain their own history ACL plus current Project access. JSON version4 fences new contracts across SQLite rollback; code downgrades must not erase receipts. No deployment or UI replacement. See docs/subsystems/project-compatibility.md.
2 changes: 2 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,5 @@ New shared Tasks have Chief admission and versioned requirements. Existing Task/
Native prior-host reconciliation now reuses verified supervisor process-group facts; native v2 registered-work/lineage and shared adapter-exit regressions guard against premature initiative transfer. In-flight current-rights abort and composed-stack acceptance remain active.

Canonical ongoing role/resource authority checks now reuse native step/SDK input and the existing scheduler + pool stop. Frozen role context remains stable; current revoked rights stop execution, with unknown exit held. Compose descendants and verify parent Task resume/API projections next.

#2186 adds the original-capability inventory and committed legacy cutover on the existing Store, group/Chief channels and Workflow controller. Validate same-version JSON/SQLite rollback separately from real-provider and UI acceptance; final real backend packaging/README #2189 follows.
1 change: 1 addition & 0 deletions changelog.d/2186.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Preserve original personal Chief, groups and Task/Workflow histories while fencing new Project work through Chief admission and durable Handoff. Retain private group permissions, committed legacy continuations and the new-state format guard across JSON/SQLite migration and rollback, with a scoped compatibility diagnostic.
3 changes: 2 additions & 1 deletion core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { join } from 'node:path'
import * as diagnostics from './src/host/diagnostics.js'
import * as store from './src/host/store.js'
import * as projectEffects from './src/host/project-effects.js'
import * as projectCompatibility from './src/host/project-compatibility.js'
import * as projectInputs from './src/host/project-inputs.js'
import * as projectSources from './src/host/project-sources.js'
import * as projectExecutionBoundary from './src/host/project-execution-boundary.js'
Expand Down Expand Up @@ -83,7 +84,7 @@ export const inject = ['llm', 'tools', 'systemPrompt', 'sessionController', 'ses
// the functions other modules call; they call them through `rt` at run time, never during
// install. Listeners run and tools are listed in registration order, so this order holds:
// usage meters model requests after every listener that rewrites them.
const MODULES = [diagnostics, store, projectEffects, projectTaskLifecycle, projectInputs, harness, memberAuth, platformIdentity, projectAccess, projectDevelopment, projectExecutionBoundary, projectResources, projectHarness, projectRuntimeSetup, projectDelivery, projectDelegations, projectAgents, projectCredentials, projectGithubRead, projectPools, projectScheduledTasks, projectSources, projectChiefControl, projectScheduler, workbenchState, authz, personalAgentAuth, models, permissions, turns, busyAck, steeringInterrupt, delivery, roster, folders, agents, prompt, timeContext, tools, decisions, projectState, meetings, chatIndex, groups, roomMembership, reach, tasks, projectBoard, taskDrive, approvals, resume, memory, parts, images, secrets, connectors, connectorAccess, capabilities, schedules, projectAssistant, projectChief, projectTeam, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, workflowStore, channel]
const MODULES = [diagnostics, store, projectEffects, projectCompatibility, projectTaskLifecycle, projectInputs, harness, memberAuth, platformIdentity, projectAccess, projectDevelopment, projectExecutionBoundary, projectResources, projectHarness, projectRuntimeSetup, projectDelivery, projectDelegations, projectAgents, projectCredentials, projectGithubRead, projectPools, projectScheduledTasks, projectSources, projectChiefControl, projectScheduler, workbenchState, authz, personalAgentAuth, models, permissions, turns, busyAck, steeringInterrupt, delivery, roster, folders, agents, prompt, timeContext, tools, decisions, projectState, meetings, chatIndex, groups, roomMembership, reach, tasks, projectBoard, taskDrive, approvals, resume, memory, parts, images, secrets, connectors, connectorAccess, capabilities, schedules, projectAssistant, projectChief, projectTeam, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, workflowStore, channel]

export function apply(ctx, config = {}) {
const rt = createRuntime(ctx, config)
Expand Down
7 changes: 6 additions & 1 deletion core/src/host/project-access.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function install(rt) {
queueDepth: queue.length, messages: [], preview: null }
}) }
}
async function workflow(body, actor) {
async function workflow(body, actor, controllerAttempt) {
const id = String(body.projectId ?? '')
const operation = body.operation ?? 'state'
const actions = { state: 'read', templates: 'read', create: 'configure', graph: 'configure', concurrency: 'configure',
Expand All @@ -81,6 +81,9 @@ export function install(rt) {
if (!task || task.projectId !== id) throw refusal('A Project Run needs a Task from this Project')
rt.requireProjectAccess(actor, id, 'task-write', task)
rt.projectTaskLifecycle?.requireExecutable(task)
if (task.projectTask && (!controllerAttempt || ownEntry(state.projectAttempts ?? {}, controllerAttempt.id) !== controllerAttempt
|| controllerAttempt.taskId !== task.id || controllerAttempt.projectId !== id || controllerAttempt.workflowId !== item.id
|| task.deliveryAttemptId !== controllerAttempt.id || controllerAttempt.status !== 'running')) throw refusal('Use task.run so the Project Task controller owns execution and Handoff')
payload = { workflowId: item.id, taskId: task.id, title: task.title, brief: task.note ?? task.title, projectId: id, callerBotId: project.chief?.botId }
const existing = activeRuns(id).find(run => run.taskId === task.id)
if (existing) {
Expand Down Expand Up @@ -110,6 +113,8 @@ export function install(rt) {
workflowTail = result.then(() => {}, () => {})
return result
}
rt.assignProjectDeliveryWorkflow = (attempt, actor) => workflow({ projectId: attempt.projectId, operation: 'assign', workflowId: attempt.workflowId, taskId: attempt.taskId }, actor, attempt)

rt.projectAccessEndpoint = async (endpoint, body = {}, actor) => {
if (!rt.sharedProjectMode()) return undefined
if (!actor?.memberId || !ownEntry(state.members, actor.memberId)) throw refusal('Sign in first')
Expand Down
65 changes: 65 additions & 0 deletions core/src/host/project-compatibility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Keep original records authoritative; only pre-cutover Project work may use legacy drivers.
import { createHash, randomUUID } from 'node:crypto'
import { ownEntry } from './store.js'

const VERSION = 'project-compatibility/v1'
const fail = message => Object.assign(new Error(message), { code: 'bot/compatibility' })
const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
const identity = task => ({ taskId: task.id, projectId: task.projectId, requesterId: task.requesterId ?? null, createdAt: task.createdAt ?? null })
const active = task => !['accepted', 'cancelled', 'failed'].includes(task.status)

export function install(rt) {
rt.projectEffects.register('compat.cutover', { run: row => ({ cutoverId: row.payload.cutoverId, count: row.payload.count, sha256: row.payload.sha256 }) })
function recover() {
if (!rt.sharedProjectMode?.() || rt.readOnly?.() !== undefined || rt.state.projectCompatibility) return false
const cutoverId = randomUUID(), projects = {}
for (const project of Object.values(rt.state.projects ?? {})) {
const tasks = Object.values(rt.state.tasks ?? {}).filter(task => task.projectId === project.id && !task.projectTask).sort((a, b) => a.id.localeCompare(b.id))
const sha256 = hash(tasks.map(identity))
for (const task of tasks) task.legacyProject = { version: VERSION, cutoverId, ...identity(task) }
const effect = rt.projectEffects.stage({ kind: 'compat.cutover', projectId: project.id, key: cutoverId, payload: { cutoverId, count: tasks.length, sha256 } })
projects[project.id] = { effect, count: tasks.length, sha256 }
}
rt.state.projectCompatibility = { version: VERSION, cutoverId, createdAt: Date.now(), projects }
return true
}
function committed(projectId, cutover, project) {
const effect = ownEntry(rt.state.projectEffects ?? {}, project?.effect?.id)
return Boolean(effect?.kind === 'compat.cutover' && effect.projectId === projectId && effect.payload.cutoverId === cutover?.cutoverId
&& effect.payload.count === project.count && effect.payload.sha256 === project.sha256 && rt.projectEffects.intentCommitted(project.effect))
}
function legacyAllowed(task) {
if (task.projectTask) return false
if (!task.projectId || !rt.sharedProjectMode?.() && !rt.state.projectCompatibility) return true
const cutover = rt.state.projectCompatibility, marker = task.legacyProject
const project = ownEntry(cutover?.projects ?? {}, task.projectId)
return Boolean(cutover?.version === VERSION && marker?.version === VERSION && marker.cutoverId === cutover.cutoverId
&& hash(identity(task)) === hash({ taskId: marker.taskId, projectId: marker.projectId, requesterId: marker.requesterId, createdAt: marker.createdAt })
&& project && committed(task.projectId, cutover, project))
}
function requireLegacy(task) {
if (!legacyAllowed(task)) throw fail('Project Task contract requires Chief admission; unknown post-cutover legacy work cannot execute')
}
function registerContinuation(original, child) {
requireLegacy(original)
if (!original.projectId || !rt.state.projectCompatibility) return
if (child.reworkOf !== original.id || child.projectId !== original.projectId || child.requesterId !== original.requesterId) throw fail('Legacy continuation identity changed')
child.legacyProject = { version: VERSION, cutoverId: rt.state.projectCompatibility.cutoverId, ...identity(child), derivedFrom: original.id }
}
function report(projectId, body, actor) {
rt.requireProjectAccess(actor, projectId)
const limit = body.limit ?? 50, offset = body.offset ?? 0
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(offset) || offset < 0) throw fail('Invalid compatibility page')
const tasks = Object.values(rt.state.tasks ?? {}).filter(task => task.projectId === projectId).sort((a, b) => a.id.localeCompare(b.id))
const classify = task => task.projectTask ? 'contract' : legacyAllowed(task) ? 'legacy' : 'held'
const totals = { contract: 0, legacy: 0, held: 0, active: 0 }
for (const task of tasks) { ++totals[classify(task)]; if (active(task)) ++totals.active }
const cutover = rt.state.projectCompatibility, proof = ownEntry(cutover?.projects ?? {}, projectId)
return { version: VERSION, cutoverId: cutover?.cutoverId ?? null, committed: committed(projectId, cutover, proof),
baseline: proof ? { count: proof.count, sha256: proof.sha256 } : null, totals,
tasks: tasks.slice(offset, offset + limit).map(task => ({ id: task.id, projectId, parentId: task.parentId ?? null, status: task.status, mode: classify(task),
attemptId: task.deliveryAttemptId ?? null, parentPresent: !task.parentId || ownEntry(rt.state.tasks ?? {}, task.parentId)?.projectId === projectId })),
page: { offset, limit, total: tasks.length, nextOffset: offset + limit < tasks.length ? offset + limit : null } }
}
rt.projectCompatibility = { recover, legacyAllowed, requireLegacy, registerContinuation, report }
}
4 changes: 3 additions & 1 deletion core/src/host/project-delivery.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,9 @@ export function createProjectDelivery(rt, { publisher } = {}) {
attempt.status = 'running'
if (rt.taskDeliveryState?.(task.id, attempt.id, 'working', 'Delivery started') === false) throw reject('Task ended before delivery started')
await rt.save()
const assigned = await rt.projectEndpoint('project-workflow', { projectId: project.id, operation: 'assign', workflowId: workflow.id, taskId: task.id }, actor)
const assigned = task.projectTask
? await rt.assignProjectDeliveryWorkflow(attempt, actor)
: await rt.projectEndpoint('project-workflow', { projectId: project.id, operation: 'assign', workflowId: workflow.id, taskId: task.id }, actor)
if (assigned.ok === false) throw reject(assigned.error)
const run = assigned.workflows.find(row => row.id === workflow.id)?.run
attempt.runId = run?.id ?? null
Expand Down
5 changes: 3 additions & 2 deletions core/src/host/project-development-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const operations = Object.freeze({
'execution.boundary': 'project-execution-boundary',
'pools': 'project-task-pools', 'effect.retry': 'project-effect-retry', 'comment.publish': 'project-github-comment',
})
const queries = new Set(['roles', 'role.proposals', 'consultation.get', 'consultation.list', 'handoff.get', 'handoff.list', 'task.history', 'project.snapshot', 'events', 'command.receipt', 'command.result', 'tasks', 'task', 'task.detail', 'inputs', 'sources', 'execution.boundary', 'effects', 'attempt.context', 'attempt.integration-state', 'attempt.log', 'chief.state', 'pools'])
const queries = new Set(['compatibility', 'roles', 'role.proposals', 'consultation.get', 'consultation.list', 'handoff.get', 'handoff.list', 'task.history', 'project.snapshot', 'events', 'command.receipt', 'command.result', 'tasks', 'task', 'task.detail', 'inputs', 'sources', 'execution.boundary', 'effects', 'attempt.context', 'attempt.integration-state', 'attempt.log', 'chief.state', 'pools'])
const legacy = new Set(Object.values(operations))
const readEndpoints = new Set([...queries].map(name => operations[name]).filter(Boolean))
export const developmentQuery = body => record(body) && body.version === DEVELOPMENT_PROTOCOL && body.kind === 'query' && queries.has(body.name)
Expand Down Expand Up @@ -219,7 +219,7 @@ export function createDevelopmentService(rt) {
if (!record(body) || body.version !== DEVELOPMENT_PROTOCOL || !['command', 'query'].includes(body.kind) || !record(body.payload ?? {})) throw failure('Unsupported Project development request')
const { name, projectId } = body
rt.requireProjectAccess(actor, projectId)
if (!Object.hasOwn(operations, name) && !['tasks', 'task', 'task.history', 'project.snapshot', 'events', 'command.receipt', 'command.result', 'attempt.context', 'effects'].includes(name)) throw failure('Unknown Project development operation')
if (!Object.hasOwn(operations, name) && !['compatibility', 'tasks', 'task', 'task.history', 'project.snapshot', 'events', 'command.receipt', 'command.result', 'attempt.context', 'effects'].includes(name)) throw failure('Unknown Project development operation')
if (queries.has(name) !== (body.kind === 'query')) throw failure('Operation kind does not match the contract')
if (body.kind === 'command' && !stableId(body.requestId)) throw failure('A command needs a stable requestId')
const args = { ...(body.payload ?? {}), projectId, ...(body.requestId !== undefined ? { requestId: body.requestId } : {}) }
Expand Down Expand Up @@ -255,6 +255,7 @@ export function createDevelopmentService(rt) {
let result
if (name === 'command.receipt') { if (!receipts) throw failure('Command receipts are unavailable'); result = receipts.get(projectId, args.commandRequestId, actor, args.commandName) }
else if (name === 'command.result') { if (!receipts) throw failure('Command receipts are unavailable'); result = receipts.result(projectId, args, actor) }
else if (name === 'compatibility') result = rt.projectCompatibility.report(projectId, args, actor)
else if (name === 'events') result = rt.committedProjectEvents.query(projectId, args)
else if (name === 'project.snapshot') result = rt.committedProjectEvents.snapshot(projectId, args)
else if (name === 'effects') result = { effects: rt.projectEffects.list(projectId, actor, args) }
Expand Down
11 changes: 11 additions & 0 deletions core/src/host/project-state-format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// A physical format fence prevents old JSON binaries from replaying new contracts.
const FORMAT = 'flybot-project-state/v1'
export function encodeProjectState(state) {
if (!state.projectCompatibility && !Object.values(state.tasks ?? {}).some(task => task.projectTask)) return state
return { ...state, version: 4, stateFormat: FORMAT, logicalVersion: 2 }
}
export function decodeProjectState(state) {
if (state?.version !== 4 || state.stateFormat !== FORMAT || state.logicalVersion !== 2) return state
const { stateFormat: _format, logicalVersion: _version, ...logical } = state
return { ...logical, version: 2 }
}
5 changes: 3 additions & 2 deletions core/src/host/project-state-migration.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { createHash, randomUUID } from 'node:crypto'
import { existsSync, readFileSync, renameSync } from 'node:fs'
import { join } from 'node:path'
import { encodeProjectState, decodeProjectState } from './project-state-format.js'
import { homeLock } from './home-lock.js'
import { openSqliteState } from './project-state-sqlite.js'
import { writeDurableStateFile } from './project-state-storage.js'
Expand Down Expand Up @@ -29,7 +30,7 @@ function snapshot(home, expectedMigrationId) {
const state = db.read()
if (state.version !== 2) throw fail('Unsupported exported state version')
guardOf(home, expectedMigrationId)
return { text: JSON.stringify(state, null, 2), revision: metadata.revision }
return { text: JSON.stringify(encodeProjectState(state), null, 2), revision: metadata.revision }
} finally { db.close() }
}
export function exportSqliteState({ home, output, expectedMigrationId }) {
Expand Down Expand Up @@ -59,7 +60,7 @@ export function rollbackSqliteState({ home, expectedMigrationId, resume = false
}
if (marker.version !== 'flybot-storage-rollback/v1' || !/^[a-f0-9-]{36}$/.test(marker.id) || marker.archiveFile !== `state.sqlite.rollback-${marker.id}` || marker.exportFile !== `state.rollback-${marker.id}.json` || !/^[a-f0-9]{64}$/.test(marker.sha256)) throw fail('Rollback recovery marker is malformed')
const text = readFileSync(join(home, marker.exportFile), 'utf8')
if (digest(text) !== marker.sha256 || JSON.parse(text).version !== 2) throw fail('Rollback export hash changed')
if (digest(text) !== marker.sha256 || decodeProjectState(JSON.parse(text)).version !== 2) throw fail('Rollback export hash changed')
const currentText = readFileSync(statePath, 'utf8')
if (digest(currentText) !== marker.sha256) {
guardOf(home, expectedMigrationId)
Expand Down
Loading
Loading