Skip to content

Commit ababc82

Browse files
committed
fix(workspace): reuse local Goal defaults and scope matching
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
1 parent 72b0fdc commit ababc82

10 files changed

Lines changed: 129 additions & 272 deletions

‎docs/quota-allocation.md‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,16 +1495,18 @@ Post-turn accounting protocol:
14951495
workspace cannot produce that local receipt. For originless Git, the checkout
14961496
root must equal the registered project root; nested repositories, linked
14971497
worktrees, invalid/empty origins and failed Git config reads cannot fall back.
1498-
- `quota should-run` also recognizes explicitly declared local material work:
1499-
`same_agent_non_delivery`, a non-code task domain, no task repository, and all
1500-
relative Todo write scopes contained by absolute Goal grants rooted at the
1501-
observed local project. These peer tasks keep their write scopes and use the
1502-
existing local Goal delivery snapshot instead of requiring a Git worktree.
1503-
Exact paths and recursive directory grants qualify; complex absolute globs
1504-
do not. Outside-root work receives a return-to-Goal diagnostic. Code work,
1505-
foreign repositories, undeclared contracts and explicit isolation policies
1506-
retain their existing guards. This changes local peer material admission,
1507-
not authorization, claim/lease rules or causal settlement requirements.
1498+
- `quota should-run` reuses that registered `local_goal` identity for local
1499+
tasks by default. Declaring write scopes does not turn local work into a Git
1500+
edit or require a particular task domain or continuation marker. An explicit
1501+
task repository and owner isolation requirements keep their existing guards.
1502+
Relative Goal scopes keep their existing matching semantics; absolute grants
1503+
under the registered root are projected into the same relative view, including
1504+
existing glob patterns. The existing boundary guard alone checks coverage.
1505+
Caller cwd does not rebase the declared targets or itself block local work.
1506+
If actual delivery is produced in the Goal project from another cwd, use
1507+
`refresh-state --delivery-workspace-path <Goal-project>`; settlement consumes
1508+
that recorded local workspace without requiring a cwd move. This changes local
1509+
task admission, not grants, claim/lease or causal settlement requirements.
15081510
- `todo complete --evidence <pointer>` can record a validated local artifact.
15091511
`--result-file` additionally requires approved Goal acceptance criteria bound
15101512
to that Todo. A standalone Todo validator does not establish Goal acceptance;

‎loopx/control_plane/agents/workspace_guard.py‎

Lines changed: 16 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,12 @@ def _peer_work_requires_isolated_workspace(
323323
agent_todo_summary: dict[str, Any] | None,
324324
*,
325325
selected_todo: dict[str, Any] | None = None,
326-
local_write_scope_admitted: bool = False,
326+
local_goal_workspace: bool = False,
327327
) -> bool:
328328
explicit = workspace_guard_policy.get("peer_independent_worktree_required")
329329
if explicit is not None:
330330
return explicit is True
331-
if local_write_scope_admitted:
331+
if local_goal_workspace:
332332
return False
333333
candidate = (
334334
selected_todo
@@ -347,19 +347,14 @@ def _peer_work_requires_isolated_workspace(
347347
)
348348

349349

350-
def observe_goal_local_write_scopes(
350+
def observe_goal_local_workspace(
351351
goal: dict[str, Any],
352352
selected_todo: dict[str, Any] | None,
353353
allowed_write_scopes: list[str] | None = None,
354354
) -> dict[str, Any]:
355-
"""Read physical Goal identity once; typed work requirements own admission."""
356-
empty = {"admitted": False, "allowed_write_scopes": []}
357-
if (
358-
not selected_todo
359-
or selected_todo.get("task_repository")
360-
or selected_todo.get("continuation_policy") != "same_agent_non_delivery"
361-
or not selected_todo.get("required_write_scopes")
362-
):
355+
"""Reuse the registered workspace identity and project its existing grants."""
356+
empty: dict[str, Any] = {}
357+
if not selected_todo or selected_todo.get("task_repository"):
363358
return empty
364359
repo = goal.get("repo") or goal.get("project") or goal.get("root")
365360
goal_id = goal.get("goal_id") or goal.get("id")
@@ -378,11 +373,11 @@ def observe_goal_local_write_scopes(
378373
scopes = allowed_write_scopes if allowed_write_scopes is not None else raw_scopes
379374
if not isinstance(scopes, list) or any(not isinstance(scope, str) for scope in scopes):
380375
return empty
381-
from ..quota.settlement_workspace_causality import project_goal_local_write_scopes
376+
from ..quota.settlement_workspace_causality import project_goal_write_scopes
382377

383378
# Physical identity is resolved above. Scope authority stays relative to the
384379
# registered spelling, including an explicit symlink alias of that root.
385-
return project_goal_local_write_scopes(str(root), selected_todo, scopes)
380+
return {"workspace": snapshot, **project_goal_write_scopes(str(root), scopes)}
386381

387382

388383
def build_agent_workspace_guard(
@@ -392,7 +387,7 @@ def build_agent_workspace_guard(
392387
agent_todo_summary: dict[str, Any] | None = None,
393388
selected_todo: dict[str, Any] | None = None,
394389
current_path: Path | None = None,
395-
local_write_scopes: dict[str, Any] | None = None,
390+
local_workspace: dict[str, Any] | None = None,
396391
) -> dict[str, Any] | None:
397392
if not isinstance(agent_identity, dict):
398393
return None
@@ -410,41 +405,18 @@ def build_agent_workspace_guard(
410405
else next(iter(_peer_candidate_items(agent_todo_summary)), {})
411406
)
412407
local = (
413-
local_write_scopes
414-
if local_write_scopes is not None
415-
else observe_goal_local_write_scopes(goal, candidate)
408+
local_workspace
409+
if local_workspace is not None
410+
else observe_goal_local_workspace(goal, candidate)
416411
)
417-
root = goal.get("repo") or goal.get("project") or goal.get("root")
418-
local_admitted = local.get("admitted") is True
419-
if local_admitted and root:
420-
current = capture_delivery_workspace(
421-
current_path,
422-
local_goal_id=str(goal.get("goal_id") or goal.get("id")),
423-
local_project_root=Path(str(root)),
424-
)
425-
local_admitted = bool(current and current.get("identity_kind") == "local_goal")
426-
if (
427-
not local_admitted
428-
and workspace_guard_policy.get("peer_independent_worktree_required")
429-
is not False
430-
):
431-
return {
432-
"schema_version": AGENT_WORKSPACE_GUARD_SCHEMA_VERSION,
433-
"source": "quota.should-run",
434-
"action": local["workspace_repair_action"],
435-
"current_workspace": "foreign_workspace",
436-
"required_workspace": "local_goal_workspace",
437-
"blocks_delivery": True,
438-
"agent_id": agent_identity.get("agent_id"),
439-
"repository_source": "goal.repo",
440-
"reason": "declared local writes must run from the registered Goal workspace",
441-
"required_action": "return to the registered Goal project and rerun quota should-run before local writes",
442-
}
412+
# Local declarations are relative to the registered Goal target, not the
413+
# caller cwd. Causal accounting still names the actual delivery workspace.
414+
local_admitted = (local.get("workspace") or {}).get("identity_kind") == "local_goal"
443415
if not _peer_work_requires_isolated_workspace(
444416
workspace_guard_policy,
445417
agent_todo_summary,
446418
selected_todo=selected_todo,
447-
local_write_scope_admitted=local_admitted,
419+
local_goal_workspace=local_admitted,
448420
):
449421
return None
450422
task_repository = normalize_todo_task_repository(candidate.get("task_repository"))

‎loopx/control_plane/quota/projection_repair.py‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@ def build_boundary_projection_repair_hint(
182182
candidate_should_run: bool,
183183
capability_gate: dict[str, Any] | None = None,
184184
selected_todo: dict[str, Any] | None = None,
185-
local_write_scopes: dict[str, Any] | None = None,
186185
) -> dict[str, Any] | None:
187186
if not candidate_should_run or not isinstance(agent_todo_summary, dict):
188187
return None
@@ -220,12 +219,6 @@ def build_boundary_projection_repair_hint(
220219
return None
221220
boundary = goal_boundary if isinstance(goal_boundary, dict) else {}
222221
allowed_scopes = normalize_required_write_scopes(boundary.get("write_scope"))
223-
if local_write_scopes:
224-
allowed_scopes.extend(
225-
normalize_required_write_scopes(
226-
local_write_scopes.get("allowed_write_scopes")
227-
)
228-
)
229222
missing_scopes = [
230223
scope
231224
for scope in required_scopes

‎loopx/control_plane/quota/settlement_workspace_causality.py‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,22 @@
3232
DELIVERY_WORKSPACE_REQUIREMENTS = frozenset({"required", "not_required", "unknown"})
3333

3434

35-
def project_goal_local_write_scopes(
35+
def project_goal_write_scopes(
3636
project_root: str,
37-
todo: Mapping[str, Any],
3837
allowed_scopes: list[str],
3938
) -> dict[str, Any]:
4039
"""Adapt observed local-root facts to the existing typed work owner."""
4140
result = _runtime_result(
42-
"goal_local_write_scopes",
41+
"goal_write_scopes",
4342
project_root=project_root,
44-
todo=dict(todo),
4543
allowed_scopes=allowed_scopes,
46-
).get("local_write_scopes")
44+
).get("write_scope_projection")
4745
if (
4846
not isinstance(result, Mapping)
49-
or not isinstance(result.get("admitted"), bool)
5047
or not isinstance(result.get("allowed_write_scopes"), list)
5148
or any(not isinstance(scope, str) for scope in result["allowed_write_scopes"])
5249
):
53-
raise RuntimeError("TypeScript local write scope result shape mismatch")
50+
raise RuntimeError("TypeScript Goal write scope projection shape mismatch")
5451
return dict(result)
5552

5653

‎loopx/control_plane/quota/settlement_workspace_causality.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from "../coordination/coordination_state_contract.generated.ts";
1010
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
1111
import { requireJsonObject as requiredObject } from "../runtime_decode.ts";
12-
import { projectGoalLocalWriteScopes } from "../todos/work_requirements.ts";
12+
import { projectGoalWriteScopes } from "../todos/work_requirements.ts";
1313

1414
export const DELIVERY_WORKSPACE_CAUSALITY_SCHEMA_VERSION =
1515
DELIVERY_WORKSPACE_CAUSALITY_SCHEMA;
@@ -39,7 +39,7 @@ export interface DeliveryWorkspaceCausality extends JsonObject {
3939
}
4040

4141
type DeliveryWorkspaceCausalityOperation =
42-
| "goal_local_write_scopes"
42+
| "goal_write_scopes"
4343
| "classify"
4444
| "normalize"
4545
| "event_fields"
@@ -142,7 +142,7 @@ function operation(value: unknown): DeliveryWorkspaceCausalityOperation {
142142
value === "classify" || value === "normalize" ||
143143
value === "event_fields" || value === "missing_workspace" ||
144144
value === "settlement_requirement" ||
145-
value === "from_event" || value === "goal_local_write_scopes"
145+
value === "from_event" || value === "goal_write_scopes"
146146
) return value;
147147
throw new EffectRuntimeRequestError("delivery workspace causality operation is unsupported");
148148
}
@@ -351,8 +351,8 @@ export function evaluateDeliveryWorkspaceCausality(value: unknown): JsonObject {
351351
request.expected_todo_id,
352352
"expected_todo_id",
353353
);
354-
if (selectedOperation === "goal_local_write_scopes") {
355-
return result({ local_write_scopes: projectGoalLocalWriteScopes(request) });
354+
if (selectedOperation === "goal_write_scopes") {
355+
return result({ write_scope_projection: projectGoalWriteScopes(request) });
356356
}
357357
if (selectedOperation === "classify") {
358358
return result({

‎loopx/control_plane/quota/should_run.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
from ..agents.workspace_guard import (
1818
build_agent_workspace_guard,
19-
observe_goal_local_write_scopes,
19+
observe_goal_local_workspace,
2020
)
2121
from ..quota.decision_summary import (
2222
quota_plan_items as _quota_plan_items,
@@ -117,7 +117,7 @@ def _apply_selected_todo_guards(
117117
)
118118
route = _resolve_quota_should_run_route(prepared)
119119
workspace_guard = None
120-
local_write_scopes = observe_goal_local_write_scopes(
120+
local_workspace = observe_goal_local_workspace(
121121
prepared.item, selected_todo, prepared.goal_boundary.get("write_scope", [])
122122
)
123123
if not prepared.inbox_priority_due:
@@ -127,15 +127,17 @@ def _apply_selected_todo_guards(
127127
agent_todo_summary=prepared.agent_todo_summary,
128128
selected_todo=selected_todo,
129129
current_path=workspace_path,
130-
local_write_scopes=local_write_scopes,
130+
local_workspace=local_workspace,
131131
)
132132
boundary_projection_repair = build_boundary_projection_repair_hint(
133-
prepared.goal_boundary,
133+
{**prepared.goal_boundary, "write_scope": [
134+
*prepared.goal_boundary.get("write_scope", []),
135+
*local_workspace.get("allowed_write_scopes", []),
136+
]},
134137
prepared.agent_todo_summary,
135138
candidate_should_run=bool(route.should_run),
136139
capability_gate=prepared.capability_gate,
137140
selected_todo=selected_todo,
138-
local_write_scopes=local_write_scopes,
139141
)
140142
if not workspace_guard and not boundary_projection_repair:
141143
return route

‎loopx/control_plane/todos/work_requirements.ts‎

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,29 @@
22
* These validate requirements, never grant capabilities or write authority. */
33
import type { JsonObject } from "../effect_program.ts";
44
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
5-
import { optionalNonEmptyString, requireJsonObject, requireStringArray } from "../runtime_decode.ts";
5+
import { optionalNonEmptyString, requireStringArray } from "../runtime_decode.ts";
66
import { compactPythonWhitespace, stripPythonWhitespace } from "../coordination/todo_agents.ts";
77
import { normalizeWriteScopes } from "../work_items/task_lease_acquire.ts";
88
import { isAbsolute, resolve, sep } from "node:path";
99

10-
/** A Goal-local declaration keeps relative Todo scopes. Absolute Goal grants
10+
/** Goal scope projection preserves existing relative scopes. Absolute grants
1111
* are projected only against the observed local Goal root, never by suffix or
1212
* against a different repository. This is a read projection, not a grant. */
13-
export function projectGoalLocalWriteScopes(value: JsonObject): JsonObject {
13+
export function projectGoalWriteScopes(value: JsonObject): JsonObject {
1414
const root = optionalNonEmptyString(value.project_root, "project_root");
15-
const empty = { admitted: false, allowed_write_scopes: [] };
16-
if (!root || !isAbsolute(root) || value.todo == null) return empty;
17-
const todo = requireJsonObject(value.todo, "local write Todo");
18-
if (todo.task_repository || !todo.task_domain || todo.task_domain === "code" ||
19-
todo.continuation_policy !== "same_agent_non_delivery") return empty;
20-
const required = requireStringArray(todo.required_write_scopes ?? [], "required_write_scopes");
21-
if (!required.length || required.some(scope => !normalizeWriteScopes([scope]).length ||
22-
scope.includes("\\") || scope.split("/").includes("."))) return empty;
15+
if (!root || !isAbsolute(root)) return { allowed_write_scopes: [] };
2316
const prefix = resolve(root).replaceAll(sep, "/").replace(/\/+$/, "") + "/";
2417
const allowed: string[] = [];
2518
for (const raw of requireStringArray(value.allowed_scopes ?? [], "allowed_scopes")) {
2619
const scope = raw.replaceAll(sep, "/");
27-
// Do not resolve a grant's dot segments: that would change its authority.
28-
if (!scope.startsWith(prefix) || scope.includes("\\") || scope.split("/").some(part => part === "." || part === "..")) continue;
29-
const relative = scope.slice(prefix.length);
30-
if (!normalizeWriteScopes([relative]).length) continue;
31-
// Exact paths and recursive directory grants have unambiguous containment.
32-
// Complex glob grants stay on their existing path rather than widening here.
33-
const base = relative.endsWith("/**") ? relative.slice(0, -3) : relative;
34-
if (/[\[*?]/.test(base)) continue;
35-
if (!allowed.includes(relative)) allowed.push(relative);
20+
// Only strip the registered root. Preserve the existing relative/glob
21+
// semantics; the boundary guard, not this projection, checks coverage.
22+
const relative = isAbsolute(scope)
23+
? (scope.startsWith(prefix) ? scope.slice(prefix.length) : "")
24+
: scope;
25+
if (normalizeWriteScopes([relative]).length && !allowed.includes(relative)) allowed.push(relative);
3626
}
37-
return {
38-
admitted: required.every(scope => allowed.some(grant =>
39-
scope === grant || (grant.endsWith("/**") && scope.startsWith(grant.slice(0, -2))))),
40-
allowed_write_scopes: allowed,
41-
workspace_repair_action: "move_to_goal_workspace",
42-
};
27+
return { allowed_write_scopes: allowed };
4328
}
4429

4530
function optionalText(value: unknown, label: string): string | null {

‎loopx/control_plane/work_items/interaction_contract.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1493,7 +1493,7 @@ def _build_interaction_cli_channel(
14931493
"refresh": (
14941494
"delivery_workspace; otherwise --delivery-workspace-path"
14951495
if selected_todo.get("task_repository")
1496-
else "registered local Goal workspace; follow the current workspace guard and repository rules"
1496+
else "registered local Goal target; when caller cwd differs, refresh with --delivery-workspace-path for the actual target; spend uses the recorded delivery workspace"
14971497
),
14981498
"spend": "recorded_delivery_workspace",
14991499
"mismatch": "fail_closed",

0 commit comments

Comments
 (0)