Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions services/android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,12 @@ new turn in that same conversation, including questions and follow-ups about pri
There is no automatic reflection, skill-writing instruction, task recipe, or result schema.
Bundled and host skill discovery are disabled. The only task prompt is the phone location,
owner-agent role and caller goal/context. Existing dedicated native owner preferences remain.
The nonsecret caller input preserves `goal`, `owner_instruction` verbatim (including its
restrictions), typed `params`, `apps` and requested `outputs` in a small JSON envelope. These
are caller-supplied instructions, not an independent owner authorization. Principal/device/request
provenance and enforced run limits stay in transport/storage; lease/auth tokens and secret
declarations are excluded from model input. Natural replies remain valid, without a required
output format.

The caller still acquires `android_session`, submits a durable `request_id` with `start`,
and follows that request with `get`. An idle conversation holds no phone lease or device lock.
Expand Down
15 changes: 8 additions & 7 deletions services/android/src/deedkit_android/agent_bare.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ def __init__(self, *args):

def prompt(self):
host = self.agents.read_config()
request = self.request
# Use the validated request, not executor substitutions: those stringify bools/ints.
# Lease/auth tokens and secret declarations have no place in model input.
body = {name: getattr(request, name) for name in (
'goal', 'owner_instruction', 'params', 'apps', 'outputs')}
prompt = (f'Android phone on this host: {host.adb} -s {host.devices[self.device]}.\n'
"You are the owner's agent that does orders and tasks on this phone.\n"
f'Goal: {self.request.goal}\n')
if self.params:
prompt += 'Parameters: ' + json.dumps(self.params) + '\n'
if self.request.apps:
prompt += 'Apps: ' + ', '.join(self.request.apps) + '\n'
return prompt
'Caller input (nonsecret JSON):\n')
return prompt + json.dumps(body, ensure_ascii=False) + '\n'

def skill_files(self):
"""Change metadata only. Codex authors and discovers ordinary skills itself."""
Expand Down Expand Up @@ -201,7 +202,7 @@ async def final(self, state, extra):
'model_ms': max(0, self.engine_ms - self.device_ms)},
'tokens': self.stream.tokens(), 'token_usage_observed': observed,
'engine': 'codex', 'model': self.config.model,
'effort': self.config.reasoning_effort, 'mode': 'bare', 'prompt_version': 1,
'effort': self.config.reasoning_effort, 'mode': 'bare', 'prompt_version': 2,
'skills_updated': self.skills_updated, 'learning_error': self.learning_error,
'engine_errors': agent_diagnostics.receipt(self.stream.errors, bytes(self.stderr),
[self.request.goal, self.request.owner_instruction, *self.params.values()]), **extra}
5 changes: 4 additions & 1 deletion services/android/src/deedkit_android/agent_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,10 @@ async def start(self, owner, call):
identity = call.identity()
existing = self.db.row(owner, call.request_id)
if existing is not None:
if existing['identity'] != identity:
# Keep legacy identities usable while distinguishing exact typed model input.
saved_params = json.loads(existing['request']).get('params', {})
if existing['identity'] != identity or json.dumps(saved_params, sort_keys=True) != json.dumps(
call.params, sort_keys=True):
raise Refused('request_id_conflict')
return self.view(owner, call.request_id, 0)
config = self.read_config()
Expand Down
2 changes: 1 addition & 1 deletion services/android/tests/test_agent_bare.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_bare_default_launch_result_and_mandatory_skill_reuse(tmp_path):
assert 'sandbox_workspace_write.network_access=true' in argv
assert 'approval_policy=never' in argv
assert not any('mcp_servers' in item or 'dangerously' in item or 'ignore-rules' in item for item in argv)
assert 'SERIAL' in prompt and 'Order check for me' not in prompt
assert 'SERIAL' in prompt and 'Order check for me' in prompt
assert service.phone.commands == [] # no guard, hierarchy, or phone MCP
home = Path(seen['env']['CODEX_HOME'])
skill = home / 'skills' / 'example-food' / 'SKILL.md'
Expand Down
55 changes: 55 additions & 0 deletions services/android/tests/test_session_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Exact caller input at the engine protocol boundary; synthetic engine and phone only."""

import json
import sqlite3

import pytest

from agent_harness import Service, recorded
from test_agent_bare import bare_host
from test_codex_session import calls, live_host


# spec: ANYM-993
@pytest.mark.parametrize('persistent', [False, True])
def test_exact_nonsecret_input_and_provenance_reach_engine(tmp_path, persistent):
config, record = (live_host if persistent else bare_host)(tmp_path)
fields = dict(goal='Read basket\nthen ask which address.',
owner_instruction='Не выбирай товар до уточнения.\nKeep courier contact until received.',
params={'COUNT': 2, 'CONFIRMED': False, 'NOTE': 'Keep "original"\nwords'},
apps=['example.food'], outputs=['total'], max_minutes=3, max_actions=20)
with Service(config) as service:
session = service.lease()
start = service.start(session, **fields)
assert service.finished(start['request_id'])['state'] == 'succeeded'
if persistent:
turn, = [row['message'] for row in calls(record) if row['message']['method'] == 'turn/start']
prompt = turn['params']['input'][0]['text']
assert turn['params']['approvalPolicy'] == 'never'
else:
prompt = recorded(record)['stdin']
body = json.loads(prompt.split('Caller input (nonsecret JSON):\n', 1)[1])
assert body == {key: value for key, value in fields.items() if key not in {'max_minutes', 'max_actions'}}
assert type(body['params']['COUNT']) is int and type(body['params']['CONFIRMED']) is bool
assert session not in prompt # device lease token is transport-only
with sqlite3.connect(tmp_path / 'state' / 'state.sqlite3') as db:
db.row_factory = sqlite3.Row
row = db.execute('SELECT * FROM agent_runs WHERE owner=? AND id=?', ('one', start['request_id'])).fetchone()
saved = json.loads(row['request'])
assert row['owner'] == 'one' and row['device'] == 'home'
assert saved['request_id'] == start['request_id']
assert all(saved[key] == value for key, value in fields.items())


# spec: ANYM-993
def test_changed_parameter_type_conflicts_without_another_native_turn(tmp_path):
config, record = live_host(tmp_path)
with Service(config) as service:
session = service.lease()
first = service.start(session, params={'COUNT': 2})
service.finished(first['request_id'])
replay = service.start(session, request_id=first['request_id'], params={'COUNT': 2})
assert replay['request_id'] == first['request_id']
refused = service.start(session, request_id=first['request_id'], params={'COUNT': '2'}, code=409)
assert refused['error'] == 'request_id_conflict'
assert len([row for row in calls(record) if row['message']['method'] == 'turn/start']) == 1
5 changes: 4 additions & 1 deletion tests/test_backup_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,11 @@ def test_concurrent_cli_cannot_recover_or_stop_lock_owners_job(tmp_path,monkeypa
monkeypatch.setattr(sys,'argv',['backup_runner','--config','unused'])
monkeypatch.setattr(backup_runner.signal,'signal',lambda *_:None)
before=(tmp_path/'journal.json').read_bytes()
previous_mask=os.umask(0o077)
try:
with pytest.raises(SystemExit):backup_runner.main()
finally:os.close(lock)
finally:
os.umask(previous_mask)
os.close(lock)
assert called==[] and docker.calls==[]
assert (tmp_path/'journal.json').read_bytes()==before
3 changes: 3 additions & 0 deletions tests/test_backup_recordings_staging.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,10 +563,13 @@ def run_main(monkeypatch, tmp_path, runner_cfg, *argv):
config.chmod(0o600)
monkeypatch.setattr(sys, 'argv', ['backup_runner', '--config', str(config), *argv])
monkeypatch.setattr(backup_runner.signal, 'signal', lambda *_: None)
previous_mask = os.umask(0o077)
try:
backup_runner.main()
except SystemExit as stopped:
return stopped.code
finally:
os.umask(previous_mask)
return 0


Expand Down
22 changes: 21 additions & 1 deletion tests/test_inference_egress.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ def namespace_ready():


def in_namespace(script, timeout=120):
return subprocess.run(['sudo', '-n', 'unshare', '-n', 'sh', '-ec', script], text=True, capture_output=True,
# Only synthetic results in the caller's private tmp_path: sudo may inherit a caller's 0077,
# which makes root-created captures/state unreadable to that caller. Leave the parent mask alone.
return subprocess.run(['sudo', '-n', 'unshare', '-n', 'sh', '-ec', 'umask 022; ' + script], text=True, capture_output=True,
timeout=timeout)


Expand Down Expand Up @@ -289,6 +291,24 @@ def test_ufw_form_passes_iptables_restore_test_for_syntax(tmp_path):
assert result.returncode == 0, result.stderr


# spec: ANYM-693
@needs_namespace
def test_namespace_outputs_ignore_caller_umask_without_hiding_exit_or_errors(tmp_path):
output, state = tmp_path / 'namespace-output', tmp_path / 'raw-state.json'
hook = after_init(tmp_path)
previous = os.umask(0o077)
try:
result = in_namespace(f'umask; printf fixture > {output}; DEEDKIT_RAW_STATE={state} sh {hook} start; '
'printf fixture-error >&2; exit 17')
parent_mask = os.umask(0o077)
finally:
os.umask(previous)
assert result.returncode == 17 and result.stderr == 'fixture-error'
assert result.stdout.strip() == '0022' and parent_mask == 0o077
assert output.stat().st_mode & 0o777 == state.stat().st_mode & 0o777 == 0o644
assert output.read_text() == 'fixture' and json.loads(state.read_text())['ok'] is True


# spec: ANYM-693
@needs_namespace
def test_ufw_block_restores_once_in_a_disposable_namespace(tmp_path):
Expand Down
Loading